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}