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