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}