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