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 /// Start one detached run with an exact host-selected run id.
138 ///
139 /// Replaying the same id, session, and prompt returns the existing run
140 /// without executing it again. Reusing the id for different immutable
141 /// input fails with [`crate::error::CodeError::RunIdentityConflict`].
142 /// This is the canonical entry point for headless `a3s code` hosts.
143 pub async fn spawn_run_with_id(&self, run_id: &str, prompt: &str) -> Result<AgentRunSpawn> {
144 conversation_runtime::spawn_run_with_id(self, run_id, prompt).await
145 }
146
147 /// Resume a durable checkpoint into one exact, fresh host-selected run id.
148 ///
149 /// The checkpoint run remains immutable. Replaying the same recovery run
150 /// id returns its existing snapshot and never executes the checkpoint
151 /// twice.
152 pub async fn spawn_recovery_with_run_id(
153 &self,
154 checkpoint_run_id: &str,
155 run_id: &str,
156 ) -> Result<AgentRunSpawn> {
157 conversation_runtime::spawn_recovery_with_run_id(self, checkpoint_run_id, run_id).await
158 }
159
160 /// Send a prompt with image attachments and wait for the complete response.
161 ///
162 /// Images are included as multi-modal content blocks in the user message.
163 /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
164 pub async fn send_with_attachments(
165 &self,
166 prompt: &str,
167 attachments: &[crate::llm::Attachment],
168 history: Option<&[Message]>,
169 ) -> Result<AgentResult> {
170 conversation_runtime::send_with_attachments(self, prompt, attachments, history).await
171 }
172
173 /// Stream a prompt with image attachments.
174 ///
175 /// Images are included as multi-modal content blocks in the user message.
176 /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
177 pub async fn stream_with_attachments(
178 &self,
179 prompt: &str,
180 attachments: &[crate::llm::Attachment],
181 history: Option<&[Message]>,
182 ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
183 conversation_runtime::stream_with_attachments(self, prompt, attachments, history).await
184 }
185
186 /// Send a prompt and stream events back.
187 ///
188 /// When `history` is `None`, uses the session's internal history
189 /// and updates it when the stream completes.
190 /// When `Some`, uses the provided history instead.
191 ///
192 /// If the prompt starts with `/`, it is dispatched as a slash command
193 /// and the result is emitted as a single `TextDelta` + `End` event.
194 pub async fn stream(
195 &self,
196 prompt: &str,
197 history: Option<&[Message]>,
198 ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
199 conversation_runtime::stream(self, prompt, history).await
200 }
201
202 /// Cancel the current ongoing operation (send/stream).
203 ///
204 /// If an operation is in progress, this will trigger cancellation of the LLM streaming
205 /// and tool execution. The operation will terminate as soon as possible.
206 ///
207 /// Returns `true` if an operation was cancelled, `false` if no operation was in progress.
208 pub async fn cancel(&self) -> bool {
209 RunControl::from_session(self).cancel_current().await
210 }
211
212 /// Cancel the current operation and wait for its single-flight lease to be
213 /// released. Streaming workers first receive cooperative cancellation; a
214 /// worker that exceeds `grace` is aborted, then given `abort_grace` to run
215 /// destructors and release admission ownership.
216 ///
217 /// Returns `true` once the session is safe to reuse. A blocking `send`
218 /// future cannot be force-aborted by the session and may return `false` if
219 /// its caller does not poll it to completion.
220 pub async fn cancel_and_settle(
221 &self,
222 grace: std::time::Duration,
223 abort_grace: std::time::Duration,
224 ) -> bool {
225 let _ = self.cancel().await;
226 if self.run_admission.wait_until_idle(grace).await {
227 return true;
228 }
229 if !self.run_admission.abort_stream_workers() {
230 return false;
231 }
232 self.run_admission.wait_until_idle(abort_grace).await
233 }
234
235 /// Return a snapshot of the session's conversation history.
236 pub fn history(&self) -> Vec<Message> {
237 SessionView::from_session(self).history()
238 }
239
240 /// Return a reference to the session's memory.
241 ///
242 /// Normal sessions always have memory; `None` is reserved for
243 /// lower-level/manual construction compatibility.
244 pub fn memory(&self) -> Option<&Arc<crate::memory::AgentMemory>> {
245 SessionView::from_session(self).memory()
246 }
247
248 /// Return the session ID.
249 pub fn id(&self) -> &str {
250 SessionView::from_session(self).id()
251 }
252
253 /// Return the session workspace path.
254 pub fn workspace(&self) -> &std::path::Path {
255 SessionView::from_session(self).workspace()
256 }
257
258 /// Return any deferred init warning (e.g. memory store failed to initialize).
259 pub fn init_warning(&self) -> Option<&str> {
260 SessionView::from_session(self).init_warning()
261 }
262
263 /// Return the session ID.
264 pub fn session_id(&self) -> &str {
265 SessionView::from_session(self).id()
266 }
267
268 /// The session's persistence store, if one is configured — needed by the
269 /// resumable orchestration combinator to journal workflow progress.
270 pub fn session_store(&self) -> Option<Arc<dyn crate::store::SessionStore>> {
271 self.session_store.clone()
272 }
273
274 /// Return the model-visible definitions for this session.
275 ///
276 /// The list reflects the live state of the tool executor — tools added via
277 /// `add_mcp_server()` appear immediately; tools removed via
278 /// `remove_mcp_server()` disappear immediately. Hidden host compatibility
279 /// aliases remain executable by name but are omitted to avoid schema cost.
280 pub fn tool_definitions(&self) -> Vec<crate::llm::ToolDefinition> {
281 DirectToolRuntime::from_session(self).definitions()
282 }
283
284 /// Return the names of all model-visible tools on this session.
285 ///
286 /// Equivalent to `tool_definitions().into_iter().map(|t| t.name).collect()`.
287 /// Tools added via [`Self::add_mcp_server`] appear immediately; tools
288 /// removed via [`Self::remove_mcp_server`] disappear immediately.
289 pub fn tool_names(&self) -> Vec<String> {
290 DirectToolRuntime::from_session(self).names()
291 }
292
293 /// Return a stored tool artifact by URI, if it exists in this session.
294 pub fn get_artifact(&self, artifact_uri: &str) -> Option<crate::tools::ToolArtifact> {
295 DirectToolRuntime::from_session(self).artifact(artifact_uri)
296 }
297
298 /// Return compact execution trace events recorded for this session.
299 pub fn trace_events(&self) -> Vec<crate::trace::TraceEvent> {
300 SessionView::from_session(self).trace_events()
301 }
302
303 /// Save the session to the configured store.
304 ///
305 /// Returns `Ok(())` if saved successfully, or if no store is configured (no-op).
306 pub async fn save(&self) -> Result<()> {
307 session_save::save(self).await
308 }
309}