Skip to main content

opencode_ctl/tools/
messages.rs

1//! `session_prompt` and `session_messages`.
2
3use serde_json::{json, Value};
4
5use crate::clock::Clock;
6use crate::compact::{compact_message, ToolsMode};
7use crate::engine::Engine;
8use crate::error::{ErrorCode, ToolError, ToolResult};
9use crate::transport::Transport;
10
11use super::{SessionMessagesArgs, SessionPromptArgs};
12
13fn invalid(msg: String) -> ToolError {
14    ToolError::new(ErrorCode::InvalidArgument, msg)
15}
16
17impl<T: Transport, C: Clock> Engine<T, C> {
18    pub async fn session_prompt(&self, a: SessionPromptArgs) -> ToolResult {
19        let id = self.resolve_session(a.session_id.as_deref())?;
20        self.guard_created(&id)?;
21        let delivery = a.delivery.as_deref().unwrap_or("steer");
22        if delivery != "steer" && delivery != "queue" {
23            return Err(invalid(format!(
24                "delivery '{delivery}' is not steer or queue"
25            )));
26        }
27        let body = json!({"text": a.text, "delivery": delivery});
28        if body.to_string().len() > self.settings.body_max_bytes {
29            return Err(ToolError::new(
30                ErrorCode::BodyTooLarge,
31                format!(
32                    "the prompt body exceeds body_max_bytes ({})",
33                    self.settings.body_max_bytes
34                ),
35            )
36            .hint("send a shorter prompt, or put the detail in a file the worker can read"));
37        }
38        let (data, _, _) = self
39            .call_json(
40                "session.prompt",
41                vec![("sessionID".to_string(), id)],
42                Some(body),
43            )
44            .await?;
45        Ok(json!({
46            "message_id": data.get("id").cloned().unwrap_or(Value::Null),
47            "delivery": delivery,
48            "hint": "call session_wait with after=<message_id> on EVERY slice",
49        }))
50    }
51
52    pub async fn session_messages(&self, a: SessionMessagesArgs) -> ToolResult {
53        let id = self.resolve_session(a.session_id.as_deref())?;
54        let tools = match a.tools.as_deref().unwrap_or("summary") {
55            "none" => ToolsMode::None,
56            "summary" => ToolsMode::Summary,
57            "full" => ToolsMode::Full,
58            other => {
59                return Err(invalid(format!(
60                    "tools '{other}' is not none, summary or full"
61                )))
62            }
63        };
64        let order = match a.order.as_deref().unwrap_or("desc") {
65            o @ ("asc" | "desc") => o.to_string(),
66            other => return Err(invalid(format!("order '{other}' is not asc or desc"))),
67        };
68        let mut params = vec![
69            ("sessionID".to_string(), id),
70            (
71                "limit".to_string(),
72                a.limit.unwrap_or(20).clamp(1, 200).to_string(),
73            ),
74        ];
75        match a.cursor {
76            // The cursor carries its own order and the route rejects an `order` beside it.
77            Some(c) => params.push(("cursor".into(), c)),
78            None => params.push(("order".into(), order)),
79        }
80        if let Some(t) = a.r#type {
81            params.push(("type".into(), t));
82        }
83        let (data, cursor, _) = self.call_json("session.message.list", params, None).await?;
84        let reasoning = a.reasoning.unwrap_or(false);
85        let messages: Vec<Value> = data
86            .as_array()
87            .map(|rows| {
88                rows.iter()
89                    .map(|r| {
90                        serde_json::to_value(compact_message(r, reasoning, tools))
91                            .unwrap_or(Value::Null)
92                    })
93                    .collect()
94            })
95            .unwrap_or_default();
96        let next = cursor
97            .as_ref()
98            .and_then(|c| c.get("next"))
99            .cloned()
100            .unwrap_or(Value::Null);
101        // `truncated` becomes true (with `original_bytes`) when the final cap cuts the list.
102        Ok(json!({"messages": messages, "next_cursor": next, "truncated": false}))
103    }
104}