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.current_cognitive_package_binding(),
12            )
13            .field("auto_save", &self.auto_save)
14            .finish()
15    }
16}
17
18impl AgentSession {
19    /// Return current occupancy of the scheduler shared with sibling sessions.
20    pub async fn task_scheduler_stats(
21        &self,
22    ) -> std::result::Result<
23        crate::task_scheduler::TaskSchedulerStats,
24        crate::task_scheduler::TaskSchedulerError,
25    > {
26        self.task_scheduler.stats().await
27    }
28
29    /// Get a snapshot of command entries (name, description, optional usage).
30    ///
31    /// Acquires the command registry lock briefly and returns owned data.
32    pub fn command_registry(&self) -> std::sync::MutexGuard<'_, CommandRegistry> {
33        session_commands::registry(self)
34    }
35
36    /// Register a custom slash command.
37    ///
38    /// Takes `&self` so it can be called on a shared `Arc<AgentSession>`.
39    pub fn register_command(
40        &self,
41        cmd: Arc<dyn crate::commands::SlashCommand>,
42    ) -> crate::error::Result<()> {
43        self.close_handle.mutate_immediate(|| {
44            self.ensure_compatibility_name_available(
45                crate::capability::CapabilityKind::Command,
46                cmd.name(),
47            )?;
48            session_commands::register(self, cmd);
49            Ok(())
50        })?
51    }
52
53    /// Return whether [`close`](Self::close) has been called on this session.
54    ///
55    /// Once closed, `send`/`stream` and their attachment variants fast-fail
56    /// with [`crate::error::CodeError::SessionClosed`] instead of starting a
57    /// new run.
58    pub fn is_closed(&self) -> bool {
59        self.closed.load(std::sync::atomic::Ordering::Acquire)
60    }
61
62    /// Clone the session-level [`CancellationToken`](tokio_util::sync::CancellationToken).
63    ///
64    /// All in-flight runs derive their per-operation token from this one via
65    /// `child_token()`, so embedders can:
66    ///
67    /// - Observe the token (e.g. wire it into a host-side `select!`) to
68    ///   react to session shutdown without polling [`is_closed`](Self::is_closed);
69    /// - Call `.cancel()` on it to abort every operation in the session
70    ///   without going through `close()` (no run-store / hook side effects).
71    ///
72    /// For graceful shutdown prefer [`close`](Self::close), which also marks
73    /// runs as cancelled in the store and notifies the configured hook executor.
74    pub fn session_cancel_token(&self) -> tokio_util::sync::CancellationToken {
75        self.session_cancel.clone()
76    }
77
78    /// Observe asynchronous semantic workspace indexing for this session.
79    ///
80    /// Disabled sessions return a stable `disabled` status without starting a
81    /// background task. Closing a configured session transitions the same
82    /// status to `closed` and releases its in-memory vector index.
83    pub fn workspace_retrieval_status(&self) -> crate::workspace::WorkspaceRetrievalStatus {
84        self.workspace_retrieval
85            .as_ref()
86            .map(|runtime| runtime.status())
87            .unwrap_or_else(crate::workspace::WorkspaceRetrievalStatus::disabled)
88    }
89
90    /// Run a structured semantic workspace query against this session.
91    ///
92    /// Returned source chunks are reread and digest-verified through the
93    /// session's workspace backend. Partial indexing, degradation, and
94    /// revision races are represented explicitly in the result's status and
95    /// fallback fields.
96    pub async fn semantic_search(
97        &self,
98        request: crate::workspace::WorkspaceSemanticSearchRequest,
99    ) -> crate::workspace::WorkspaceRetrievalResult<crate::workspace::WorkspaceSemanticSearchResult>
100    {
101        if self.is_closed() {
102            return Err(crate::workspace::WorkspaceRetrievalError::Unavailable);
103        }
104        self.tool_context
105            .workspace_services
106            .semantic_search(request, self.session_cancel.child_token())
107            .await
108    }
109
110    /// Run structured hybrid workspace retrieval for this session.
111    ///
112    /// Exact literal, BM25, optional Code Intelligence symbols, and semantic
113    /// candidates are fused by rank. Returned chunks are current-source
114    /// verified and include per-channel status and fallback metadata.
115    pub async fn hybrid_search(
116        &self,
117        request: crate::workspace::WorkspaceHybridSearchRequest,
118    ) -> crate::workspace::WorkspaceRetrievalResult<crate::workspace::WorkspaceHybridSearchResult>
119    {
120        if self.is_closed() {
121            return Err(crate::workspace::WorkspaceRetrievalError::Unavailable);
122        }
123        self.tool_context
124            .workspace_services
125            .hybrid_search(request, self.session_cancel.child_token())
126            .await
127    }
128
129    /// Return the host-defined tenant id, if any.
130    ///
131    /// The framework only transports this string — it never interprets
132    /// or enforces tenant boundaries itself. Use this from custom
133    /// `HookExecutor` / `PermissionChecker` / `BudgetGuard` impls to route logic
134    /// by tenant.
135    pub fn tenant_id(&self) -> Option<&str> {
136        self.tenant_id.as_deref()
137    }
138
139    /// Return the principal that triggered the session, if any.
140    pub fn principal(&self) -> Option<&str> {
141        self.principal.as_deref()
142    }
143
144    /// Return the id of the agent template/definition the session was
145    /// instantiated from, if any.
146    pub fn agent_template_id(&self) -> Option<&str> {
147        self.agent_template_id.as_deref()
148    }
149
150    /// Return the distributed-trace correlation id propagated through
151    /// this session's events, if any.
152    pub fn correlation_id(&self) -> Option<&str> {
153        self.correlation_id.as_deref()
154    }
155
156    /// Return the Session-static cognitive-package binding, if configured.
157    ///
158    /// This compatibility accessor does not observe a later atomic Knowledge
159    /// projection. New host code should use
160    /// [`current_cognitive_package_binding`](Self::current_cognitive_package_binding).
161    pub fn cognitive_package_binding(
162        &self,
163    ) -> Option<&crate::cognitive_context::CognitivePackageBindingV1> {
164        self.cognitive_context
165            .as_ref()
166            .map(crate::cognitive_context::CognitiveContextSession::binding)
167    }
168
169    /// Return the exact cognitive-package binding visible to the next Run.
170    ///
171    /// The returned value is owned because the current atomic catalog
172    /// generation is pinned only for this read. An admitted Run records its
173    /// own binding and remains on that generation across later cutover.
174    pub fn current_cognitive_package_binding(
175        &self,
176    ) -> Option<crate::cognitive_context::CognitivePackageBindingV1> {
177        let projection = self.capability_catalog.pin();
178        super::agent_loop_runtime::cognitive_binding_for_projection(self, projection.projection())
179    }
180
181    /// Proactively close the session and release its in-flight work.
182    ///
183    /// On the first call this:
184    /// 1. flips the session into the **closed** state so further `send`/`stream`
185    ///    calls fast-fail with [`crate::error::CodeError::SessionClosed`];
186    /// 2. stops the optional lane queue from accepting new commands;
187    /// 3. cancels and joins session-owned memory maintenance within its bound;
188    /// 4. waits for completed-turn memory extractions accepted before close,
189    ///    up to the bounded shutdown deadline;
190    /// 5. fires the session-level cancellation token so every derived
191    ///    run/subagent token cascades to cancelled;
192    /// 5. marks the active run `Cancelled` in the run store and notifies the
193    ///    configured hook executor;
194    /// 6. cancels every still-running delegated subagent task spawned from
195    ///    this session;
196    /// 7. cancels all pending human-in-the-loop tool confirmations and external
197    ///    queue tasks, then drains commands admitted before close;
198    /// 8. disconnects MCP servers owned by this session, without touching
199    ///    inherited agent- or host-owned managers.
200    ///
201    /// Subsequent calls are no-ops and are guaranteed not to panic.
202    pub async fn close(&self) {
203        // Delegate to the shared handle so this entry point and
204        // `Agent::close_session(id)` cannot drift in behaviour.
205        self.close_handle.close().await;
206    }
207
208    /// Send a prompt and wait for the complete response.
209    ///
210    /// When `history` is `None`, uses (and auto-updates) the session's
211    /// internal conversation history. When `Some`, uses the provided
212    /// history instead (the internal history is **not** modified).
213    ///
214    /// If the prompt starts with `/`, it is dispatched as a slash command
215    /// and the result is returned without calling the LLM.
216    pub async fn send(&self, prompt: &str, history: Option<&[Message]>) -> Result<AgentResult> {
217        conversation_runtime::send(self, prompt, history).await
218    }
219
220    /// Resume a previously-checkpointed run on this session.
221    ///
222    /// Loads the latest [`LoopCheckpoint`](crate::loop_checkpoint::LoopCheckpoint)
223    /// stored under `checkpoint_run_id` and replays the agent loop from
224    /// that boundary state. A **new** run id is allocated for the
225    /// resumed work; the relationship between the old and new run is
226    /// host-tracked — the framework does not interpret
227    /// it.
228    ///
229    /// Returns an error when no `SessionStore` is configured on this
230    /// session, or when no checkpoint exists for `checkpoint_run_id`.
231    pub async fn resume_run(&self, checkpoint_run_id: &str) -> Result<AgentResult> {
232        conversation_runtime::resume_run(self, checkpoint_run_id).await
233    }
234
235    /// Start one detached run with an exact host-selected run id.
236    ///
237    /// Replaying the same id, session, and prompt returns the existing run
238    /// without executing it again. Reusing the id for different immutable
239    /// input fails with [`crate::error::CodeError::RunIdentityConflict`].
240    /// This is the canonical entry point for headless `a3s code` hosts.
241    pub async fn spawn_run_with_id(&self, run_id: &str, prompt: &str) -> Result<AgentRunSpawn> {
242        conversation_runtime::spawn_run_with_id(self, run_id, prompt).await
243    }
244
245    /// Resume a durable checkpoint into one exact, fresh host-selected run id.
246    ///
247    /// The checkpoint run remains immutable. Replaying the same recovery run
248    /// id returns its existing snapshot and never executes the checkpoint
249    /// twice.
250    pub async fn spawn_recovery_with_run_id(
251        &self,
252        checkpoint_run_id: &str,
253        run_id: &str,
254    ) -> Result<AgentRunSpawn> {
255        conversation_runtime::spawn_recovery_with_run_id(self, checkpoint_run_id, run_id).await
256    }
257
258    pub(crate) async fn prepare_recovery_with_evidence(
259        &self,
260        evidence: &crate::session_checkpoint::SessionLogicalResumeEvidenceV1,
261        checkpoint_identity: &str,
262        run_id: &str,
263    ) -> std::result::Result<super::ExactRecoveryPreparation, super::ExactRecoveryError> {
264        conversation_runtime::prepare_recovery_with_evidence(
265            self,
266            evidence,
267            checkpoint_identity,
268            run_id,
269        )
270        .await
271    }
272
273    pub(crate) async fn prepare_recovery_from_checkpoint(
274        &self,
275        evidence: &crate::session_checkpoint::SessionLogicalResumeEvidenceV1,
276        checkpoint_identity: &str,
277        run_id: &str,
278        checkpoint: crate::loop_checkpoint::LoopCheckpoint,
279    ) -> std::result::Result<super::ExactRecoveryPreparation, super::ExactRecoveryError> {
280        conversation_runtime::prepare_recovery_from_checkpoint(
281            self,
282            evidence,
283            checkpoint_identity,
284            run_id,
285            checkpoint,
286        )
287        .await
288    }
289
290    pub(crate) async fn spawn_prepared_recovery(
291        &self,
292        prepared: super::PreparedExactRecovery,
293    ) -> std::result::Result<AgentRunSpawn, super::ExactRecoveryError> {
294        conversation_runtime::spawn_prepared_recovery(self, prepared).await
295    }
296
297    pub(crate) async fn record_workspace_change_set(
298        &self,
299        run_id: &str,
300        change_set: crate::run::RunWorkspaceChangeSet,
301    ) -> Result<crate::run::RunSnapshot> {
302        let snapshot = self
303            .run_store
304            .record_workspace_change_set(run_id, change_set)
305            .await
306            .map_err(|error| anyhow::anyhow!(error))?;
307        self.save().await?;
308        Ok(snapshot)
309    }
310
311    /// Send a prompt with image attachments and wait for the complete response.
312    ///
313    /// Images are included as multi-modal content blocks in the user message.
314    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
315    pub async fn send_with_attachments(
316        &self,
317        prompt: &str,
318        attachments: &[crate::llm::Attachment],
319        history: Option<&[Message]>,
320    ) -> Result<AgentResult> {
321        conversation_runtime::send_with_attachments(self, prompt, attachments, history).await
322    }
323
324    /// Stream a prompt with image attachments.
325    ///
326    /// Images are included as multi-modal content blocks in the user message.
327    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
328    pub async fn stream_with_attachments(
329        &self,
330        prompt: &str,
331        attachments: &[crate::llm::Attachment],
332        history: Option<&[Message]>,
333    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
334        conversation_runtime::stream_with_attachments(self, prompt, attachments, history).await
335    }
336
337    /// Send a prompt and stream events back.
338    ///
339    /// When `history` is `None`, uses the session's internal history
340    /// and updates it when the stream completes.
341    /// When `Some`, uses the provided history instead.
342    ///
343    /// If the prompt starts with `/`, it is dispatched as a slash command
344    /// and the result is emitted as a single `TextDelta` + `End` event.
345    pub async fn stream(
346        &self,
347        prompt: &str,
348        history: Option<&[Message]>,
349    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
350        conversation_runtime::stream(self, prompt, history).await
351    }
352
353    /// Cancel the current ongoing operation (send/stream).
354    ///
355    /// If an operation is in progress, this will trigger cancellation of the LLM streaming
356    /// and tool execution. The operation will terminate as soon as possible.
357    ///
358    /// Returns `true` if an operation was cancelled, `false` if no operation was in progress.
359    pub async fn cancel(&self) -> bool {
360        RunControl::from_session(self).cancel_current().await
361    }
362
363    /// Cancel the current operation and wait for its single-flight lease to be
364    /// released. Streaming workers first receive cooperative cancellation; a
365    /// worker that exceeds `grace` is aborted, then given `abort_grace` to run
366    /// destructors and release admission ownership.
367    ///
368    /// Returns `true` once the session is safe to reuse. A blocking `send`
369    /// future cannot be force-aborted by the session and may return `false` if
370    /// its caller does not poll it to completion.
371    pub async fn cancel_and_settle(
372        &self,
373        grace: std::time::Duration,
374        abort_grace: std::time::Duration,
375    ) -> bool {
376        let _ = self.cancel().await;
377        if self.run_admission.wait_until_idle(grace).await {
378            return true;
379        }
380        if !self.run_admission.abort_stream_workers() {
381            return false;
382        }
383        self.run_admission.wait_until_idle(abort_grace).await
384    }
385
386    /// Return a snapshot of the session's conversation history.
387    pub fn history(&self) -> Vec<Message> {
388        SessionView::from_session(self).history()
389    }
390
391    /// Return a reference to the session's memory.
392    ///
393    /// Normal sessions always have memory; `None` is reserved for
394    /// lower-level/manual construction compatibility.
395    pub fn memory(&self) -> Option<&Arc<crate::memory::AgentMemory>> {
396        SessionView::from_session(self).memory()
397    }
398
399    /// Observe periodic pruning, semantic refresh, and host jobs for this session.
400    /// Sessions without configured maintenance return a stable `disabled`
401    /// snapshot; a closed runtime remains observable as `closed`.
402    pub fn memory_maintenance_health(&self) -> crate::memory::MemoryMaintenanceHealth {
403        self.close_handle
404            .memory_maintenance
405            .as_ref()
406            .map(|runtime| runtime.health())
407            .unwrap_or_else(crate::memory::MemoryMaintenanceHealth::disabled)
408    }
409
410    /// Return the session ID.
411    pub fn id(&self) -> &str {
412        SessionView::from_session(self).id()
413    }
414
415    /// Return the session workspace path.
416    pub fn workspace(&self) -> &std::path::Path {
417        SessionView::from_session(self).workspace()
418    }
419
420    /// Return any deferred init warning (e.g. memory store failed to initialize).
421    pub fn init_warning(&self) -> Option<&str> {
422        SessionView::from_session(self).init_warning()
423    }
424
425    /// Return the session ID.
426    pub fn session_id(&self) -> &str {
427        SessionView::from_session(self).id()
428    }
429
430    /// The session's persistence store, if one is configured — needed by the
431    /// resumable orchestration combinator to journal workflow progress.
432    pub fn session_store(&self) -> Option<Arc<dyn crate::store::SessionStore>> {
433        self.session_store.clone()
434    }
435
436    /// Return the governed source definitions available to this session.
437    ///
438    /// The list reflects the live state of the tool executor — tools added via
439    /// `add_mcp_server()` appear immediately; tools removed via
440    /// `remove_mcp_server()` disappear immediately. Hidden host compatibility
441    /// aliases remain executable by name but are omitted to avoid schema cost.
442    /// A Run applies [`Self::tool_presentation_profile`] and its frozen
443    /// permission visibility boundary before sending a subset to the model.
444    pub fn tool_definitions(&self) -> Vec<crate::llm::ToolDefinition> {
445        DirectToolRuntime::from_session(self).definitions()
446    }
447
448    /// Return the exact typed Tool-presentation profile configured for new
449    /// Runs in this session.
450    pub fn tool_presentation_profile(&self) -> &crate::tools::ToolPresentationProfileV1 {
451        &self.config.tool_presentation_profile
452    }
453
454    /// Preview the current model-facing definition projection for a prompt.
455    ///
456    /// Run admission snapshots permission providers, so this live preview is
457    /// diagnostic rather than execution authority.
458    pub fn presented_tool_definitions(
459        &self,
460        prompt: &str,
461    ) -> std::result::Result<Vec<crate::llm::ToolDefinition>, crate::tools::ToolPresentationError>
462    {
463        let mut source = self.tool_definitions();
464        if let Some(permission_checker) = &self.config.permission_checker {
465            source.retain(|tool| permission_checker.expose_to_model(&tool.name));
466        }
467        self.config
468            .tool_presentation_profile
469            .present_for_prompt(&source, prompt)
470    }
471
472    /// Return the names of all governed source tools on this session.
473    ///
474    /// Equivalent to `tool_definitions().into_iter().map(|t| t.name).collect()`.
475    /// Tools added via [`Self::add_mcp_server`] appear immediately; tools
476    /// removed via [`Self::remove_mcp_server`] disappear immediately. Use
477    /// [`Self::presented_tool_definitions`] to preview Profile projection.
478    pub fn tool_names(&self) -> Vec<String> {
479        DirectToolRuntime::from_session(self).names()
480    }
481
482    /// Return a stored tool artifact by URI, if it exists in this session.
483    pub fn get_artifact(&self, artifact_uri: &str) -> Option<crate::tools::ToolArtifact> {
484        DirectToolRuntime::from_session(self).artifact(artifact_uri)
485    }
486
487    /// Return compact execution trace events recorded for this session.
488    pub fn trace_events(&self) -> Vec<crate::trace::TraceEvent> {
489        SessionView::from_session(self).trace_events()
490    }
491
492    /// Save the session to the configured store.
493    ///
494    /// Returns `Ok(())` if saved successfully, or if no store is configured (no-op).
495    pub async fn save(&self) -> Result<()> {
496        session_save::save(self).await
497    }
498}