Skip to main content

a3s_code_core/agent_api/
session_facade.rs

1use super::*;
2
3impl std::fmt::Debug for AgentSession {
4    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5        f.debug_struct("AgentSession")
6            .field("session_id", &self.session_id)
7            .field("workspace", &self.workspace.display().to_string())
8            .field("auto_save", &self.auto_save)
9            .finish()
10    }
11}
12
13impl AgentSession {
14    /// Get a snapshot of command entries (name, description, optional usage).
15    ///
16    /// Acquires the command registry lock briefly and returns owned data.
17    pub fn command_registry(&self) -> std::sync::MutexGuard<'_, CommandRegistry> {
18        session_commands::registry(self)
19    }
20
21    /// Register a custom slash command.
22    ///
23    /// Takes `&self` so it can be called on a shared `Arc<AgentSession>`.
24    pub fn register_command(
25        &self,
26        cmd: Arc<dyn crate::commands::SlashCommand>,
27    ) -> crate::error::Result<()> {
28        self.close_handle
29            .mutate_immediate(|| session_commands::register(self, cmd))
30    }
31
32    /// Return whether [`close`](Self::close) has been called on this session.
33    ///
34    /// Once closed, `send`/`stream` and their attachment variants fast-fail
35    /// with [`crate::error::CodeError::SessionClosed`] instead of starting a
36    /// new run.
37    pub fn is_closed(&self) -> bool {
38        self.closed.load(std::sync::atomic::Ordering::Acquire)
39    }
40
41    /// Clone the session-level [`CancellationToken`](tokio_util::sync::CancellationToken).
42    ///
43    /// All in-flight runs derive their per-operation token from this one via
44    /// `child_token()`, so embedders can:
45    ///
46    /// - Observe the token (e.g. wire it into a host-side `select!`) to
47    ///   react to session shutdown without polling [`is_closed`](Self::is_closed);
48    /// - Call `.cancel()` on it to abort every operation in the session
49    ///   without going through `close()` (no run-store / hook side effects).
50    ///
51    /// For graceful shutdown prefer [`close`](Self::close), which also marks
52    /// runs as cancelled in the store and notifies the configured hook executor.
53    pub fn session_cancel_token(&self) -> tokio_util::sync::CancellationToken {
54        self.session_cancel.clone()
55    }
56
57    /// Return the host-defined tenant id, if any.
58    ///
59    /// The framework only transports this string — it never interprets
60    /// or enforces tenant boundaries itself. Use this from custom
61    /// `HookExecutor` / `PermissionChecker` / `BudgetGuard` impls to route logic
62    /// by tenant.
63    pub fn tenant_id(&self) -> Option<&str> {
64        self.tenant_id.as_deref()
65    }
66
67    /// Return the principal that triggered the session, if any.
68    pub fn principal(&self) -> Option<&str> {
69        self.principal.as_deref()
70    }
71
72    /// Return the id of the agent template/definition the session was
73    /// instantiated from, if any.
74    pub fn agent_template_id(&self) -> Option<&str> {
75        self.agent_template_id.as_deref()
76    }
77
78    /// Return the distributed-trace correlation id propagated through
79    /// this session's events, if any.
80    pub fn correlation_id(&self) -> Option<&str> {
81        self.correlation_id.as_deref()
82    }
83
84    /// Proactively close the session and release its in-flight work.
85    ///
86    /// On the first call this:
87    /// 1. flips the session into the **closed** state so further `send`/`stream`
88    ///    calls fast-fail with [`crate::error::CodeError::SessionClosed`];
89    /// 2. stops the optional lane queue from accepting new commands;
90    /// 3. waits for completed-turn memory extractions accepted before close,
91    ///    up to the bounded shutdown deadline;
92    /// 4. fires the session-level cancellation token so every derived
93    ///    run/subagent token cascades to cancelled;
94    /// 5. marks the active run `Cancelled` in the run store and notifies the
95    ///    configured hook executor;
96    /// 6. cancels every still-running delegated subagent task spawned from
97    ///    this session;
98    /// 7. cancels all pending human-in-the-loop tool confirmations and external
99    ///    queue tasks, then drains commands admitted before close;
100    /// 8. disconnects MCP servers owned by this session, without touching
101    ///    inherited agent- or host-owned managers.
102    ///
103    /// Subsequent calls are no-ops and are guaranteed not to panic.
104    pub async fn close(&self) {
105        // Delegate to the shared handle so this entry point and
106        // `Agent::close_session(id)` cannot drift in behaviour.
107        self.close_handle.close().await;
108    }
109
110    /// Send a prompt and wait for the complete response.
111    ///
112    /// When `history` is `None`, uses (and auto-updates) the session's
113    /// internal conversation history. When `Some`, uses the provided
114    /// history instead (the internal history is **not** modified).
115    ///
116    /// If the prompt starts with `/`, it is dispatched as a slash command
117    /// and the result is returned without calling the LLM.
118    pub async fn send(&self, prompt: &str, history: Option<&[Message]>) -> Result<AgentResult> {
119        conversation_runtime::send(self, prompt, history).await
120    }
121
122    /// Resume a previously-checkpointed run on this session.
123    ///
124    /// Loads the latest [`LoopCheckpoint`](crate::loop_checkpoint::LoopCheckpoint)
125    /// stored under `checkpoint_run_id` and replays the agent loop from
126    /// that boundary state. A **new** run id is allocated for the
127    /// resumed work; the relationship between the old and new run is
128    /// host-tracked — the framework does not interpret
129    /// it.
130    ///
131    /// Returns an error when no `SessionStore` is configured on this
132    /// session, or when no checkpoint exists for `checkpoint_run_id`.
133    pub async fn resume_run(&self, checkpoint_run_id: &str) -> Result<AgentResult> {
134        conversation_runtime::resume_run(self, checkpoint_run_id).await
135    }
136
137    /// Send a prompt with image attachments and wait for the complete response.
138    ///
139    /// Images are included as multi-modal content blocks in the user message.
140    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
141    pub async fn send_with_attachments(
142        &self,
143        prompt: &str,
144        attachments: &[crate::llm::Attachment],
145        history: Option<&[Message]>,
146    ) -> Result<AgentResult> {
147        conversation_runtime::send_with_attachments(self, prompt, attachments, history).await
148    }
149
150    /// Stream a prompt with image attachments.
151    ///
152    /// Images are included as multi-modal content blocks in the user message.
153    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
154    pub async fn stream_with_attachments(
155        &self,
156        prompt: &str,
157        attachments: &[crate::llm::Attachment],
158        history: Option<&[Message]>,
159    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
160        conversation_runtime::stream_with_attachments(self, prompt, attachments, history).await
161    }
162
163    /// Send a prompt and stream events back.
164    ///
165    /// When `history` is `None`, uses the session's internal history
166    /// and updates it when the stream completes.
167    /// When `Some`, uses the provided history instead.
168    ///
169    /// If the prompt starts with `/`, it is dispatched as a slash command
170    /// and the result is emitted as a single `TextDelta` + `End` event.
171    pub async fn stream(
172        &self,
173        prompt: &str,
174        history: Option<&[Message]>,
175    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
176        conversation_runtime::stream(self, prompt, history).await
177    }
178
179    /// Cancel the current ongoing operation (send/stream).
180    ///
181    /// If an operation is in progress, this will trigger cancellation of the LLM streaming
182    /// and tool execution. The operation will terminate as soon as possible.
183    ///
184    /// Returns `true` if an operation was cancelled, `false` if no operation was in progress.
185    pub async fn cancel(&self) -> bool {
186        RunControl::from_session(self).cancel_current().await
187    }
188
189    /// Cancel the current operation and wait for its single-flight lease to be
190    /// released. Streaming workers first receive cooperative cancellation; a
191    /// worker that exceeds `grace` is aborted, then given `abort_grace` to run
192    /// destructors and release admission ownership.
193    ///
194    /// Returns `true` once the session is safe to reuse. A blocking `send`
195    /// future cannot be force-aborted by the session and may return `false` if
196    /// its caller does not poll it to completion.
197    pub async fn cancel_and_settle(
198        &self,
199        grace: std::time::Duration,
200        abort_grace: std::time::Duration,
201    ) -> bool {
202        let _ = self.cancel().await;
203        if self.run_admission.wait_until_idle(grace).await {
204            return true;
205        }
206        if !self.run_admission.abort_stream_workers() {
207            return false;
208        }
209        self.run_admission.wait_until_idle(abort_grace).await
210    }
211
212    /// Return a snapshot of the session's conversation history.
213    pub fn history(&self) -> Vec<Message> {
214        SessionView::from_session(self).history()
215    }
216
217    /// Return a reference to the session's memory.
218    ///
219    /// Normal sessions always have memory; `None` is reserved for
220    /// lower-level/manual construction compatibility.
221    pub fn memory(&self) -> Option<&Arc<crate::memory::AgentMemory>> {
222        SessionView::from_session(self).memory()
223    }
224
225    /// Return the session ID.
226    pub fn id(&self) -> &str {
227        SessionView::from_session(self).id()
228    }
229
230    /// Return the session workspace path.
231    pub fn workspace(&self) -> &std::path::Path {
232        SessionView::from_session(self).workspace()
233    }
234
235    /// Return any deferred init warning (e.g. memory store failed to initialize).
236    pub fn init_warning(&self) -> Option<&str> {
237        SessionView::from_session(self).init_warning()
238    }
239
240    /// Return the session ID.
241    pub fn session_id(&self) -> &str {
242        SessionView::from_session(self).id()
243    }
244
245    /// The session's persistence store, if one is configured — needed by the
246    /// resumable orchestration combinator to journal workflow progress.
247    pub fn session_store(&self) -> Option<Arc<dyn crate::store::SessionStore>> {
248        self.session_store.clone()
249    }
250
251    /// Return the definitions of all tools currently registered in this session.
252    ///
253    /// The list reflects the live state of the tool executor — tools added via
254    /// `add_mcp_server()` appear immediately; tools removed via
255    /// `remove_mcp_server()` disappear immediately.
256    pub fn tool_definitions(&self) -> Vec<crate::llm::ToolDefinition> {
257        DirectToolRuntime::from_session(self).definitions()
258    }
259
260    /// Return the names of all tools currently registered on this session.
261    ///
262    /// Equivalent to `tool_definitions().into_iter().map(|t| t.name).collect()`.
263    /// Tools added via [`Self::add_mcp_server`] appear immediately; tools
264    /// removed via [`Self::remove_mcp_server`] disappear immediately.
265    pub fn tool_names(&self) -> Vec<String> {
266        DirectToolRuntime::from_session(self).names()
267    }
268
269    /// Return a stored tool artifact by URI, if it exists in this session.
270    pub fn get_artifact(&self, artifact_uri: &str) -> Option<crate::tools::ToolArtifact> {
271        DirectToolRuntime::from_session(self).artifact(artifact_uri)
272    }
273
274    /// Return compact execution trace events recorded for this session.
275    pub fn trace_events(&self) -> Vec<crate::trace::TraceEvent> {
276        SessionView::from_session(self).trace_events()
277    }
278
279    /// Save the session to the configured store.
280    ///
281    /// Returns `Ok(())` if saved successfully, or if no store is configured (no-op).
282    pub async fn save(&self) -> Result<()> {
283        session_save::save(self).await
284    }
285}