Skip to main content

a3s_code_core/agent_api/
governance_facade.rs

1use super::*;
2
3impl AgentSession {
4    /// Install or replace a runtime budget guard. Takes effect on the
5    /// next `send` / `stream` call (the guard is consulted at agent-
6    /// loop build time, not on the live execution). Setting `None`
7    /// clears the override so `config.budget_guard` takes over again.
8    ///
9    /// This is the entry point SDKs use to wire a host-supplied guard
10    /// after the session has already been constructed — useful when
11    /// the guard's transport (e.g. a JS callable) cannot live inside
12    /// the value-typed `SessionOptions`.
13    pub fn set_budget_guard(
14        &self,
15        guard: Option<Arc<dyn crate::budget::BudgetGuard>>,
16    ) -> crate::error::Result<()> {
17        self.close_handle.mutate_immediate(|| {
18            let mut slot = self
19                .runtime_budget_guard
20                .lock()
21                .unwrap_or_else(|p| p.into_inner());
22            *slot = guard;
23            drop(slot);
24            // Delegated children own a pre-built TaskExecutor. Refresh its parent
25            // context so the next run cannot bypass a runtime-installed ledger.
26            // SkillTool likewise owns a child AgentConfig captured at registration
27            // time, so it must be refreshed from the same runtime source of truth.
28            self.refresh_task_delegation_tools();
29            self.refresh_skill_tools();
30        })
31    }
32
33    /// Return the currently-installed runtime budget guard, if any.
34    /// `None` means the loop falls back to `config.budget_guard`.
35    pub fn budget_guard(&self) -> Option<Arc<dyn crate::budget::BudgetGuard>> {
36        self.runtime_budget_guard
37            .lock()
38            .unwrap_or_else(|p| p.into_inner())
39            .clone()
40    }
41
42    /// Override specialty prompt style for subsequent turns without rebuilding
43    /// the session. Pass `Some(AgentStyle::Plan)` for Plan; pass `None` to clear
44    /// specialty style back to GeneralPurpose. Takes effect on the next
45    /// `send` / `stream` via agent-loop build (`PROMPT-ALIGN1` hot path).
46    pub fn set_agent_style(
47        &self,
48        style: Option<crate::prompts::AgentStyle>,
49    ) -> crate::error::Result<()> {
50        self.close_handle.mutate_immediate(|| {
51            let mut slot = self
52                .runtime_agent_style
53                .lock()
54                .unwrap_or_else(|p| p.into_inner());
55            *slot = Some(style);
56            Ok(())
57        })?
58    }
59
60    /// Pin or clear the user-facing reply language for subsequent turns without
61    /// rebuilding the session. Pass `Some("zh-CN")` to pin; pass `None` to clear
62    /// the runtime override so `prompt_slots.output_language` applies again.
63    /// Takes effect on the next `send` / `stream`.
64    pub fn set_output_language(
65        &self,
66        language: Option<impl Into<String>>,
67    ) -> crate::error::Result<()> {
68        let language = language.map(Into::into).and_then(|value| {
69            let trimmed = value.trim();
70            (!trimmed.is_empty()).then(|| trimmed.to_string())
71        });
72        self.close_handle.mutate_immediate(|| {
73            let mut slot = self
74                .runtime_output_language
75                .lock()
76                .unwrap_or_else(|p| p.into_inner());
77            *slot = Some(language);
78            Ok(())
79        })?
80    }
81
82    /// Runtime agent-style override. Outer `None` means unset; inner value is
83    /// the style (or cleared specialty when `Some(None)`).
84    pub(crate) fn runtime_agent_style_override(
85        &self,
86    ) -> Option<Option<crate::prompts::AgentStyle>> {
87        *self
88            .runtime_agent_style
89            .lock()
90            .unwrap_or_else(|p| p.into_inner())
91    }
92
93    /// Runtime reply-language override. Outer `None` means unset; inner value
94    /// is the BCP-47 tag (or cleared pin when `Some(None)`).
95    pub(crate) fn runtime_output_language_override(&self) -> Option<Option<String>> {
96        self.runtime_output_language
97            .lock()
98            .unwrap_or_else(|p| p.into_inner())
99            .clone()
100    }
101
102    /// Install or clear the host-owned live checkpoint export sink (SDK-CP1).
103    ///
104    /// Takes effect on the next `send` / `stream` that opens a checkpoint
105    /// channel. Callback-backed SDK hosts use this after session construction
106    /// because value-typed `SessionOptions` cannot carry language callables.
107    pub fn set_session_checkpoint_export_sink(
108        &self,
109        sink: Option<Arc<dyn crate::session_checkpoint::SessionCheckpointExportSink>>,
110    ) -> crate::error::Result<()> {
111        self.close_handle.mutate_immediate(|| {
112            let mut slot = self
113                .runtime_session_checkpoint_export_sink
114                .lock()
115                .unwrap_or_else(|p| p.into_inner());
116            *slot = sink;
117        })
118    }
119
120    /// Return the currently installed live checkpoint export sink, if any.
121    pub fn session_checkpoint_export_sink(
122        &self,
123    ) -> Option<Arc<dyn crate::session_checkpoint::SessionCheckpointExportSink>> {
124        self.runtime_session_checkpoint_export_sink
125            .lock()
126            .unwrap_or_else(|p| p.into_inner())
127            .clone()
128    }
129
130    /// Return pending HITL tool confirmations for this session.
131    pub async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
132        HitlControl::from_session(self)
133            .pending_confirmations()
134            .await
135    }
136
137    /// Resolve a pending HITL tool confirmation.
138    ///
139    /// Returns `Ok(true)` when a pending confirmation was found and completed,
140    /// `Ok(false)` when the tool ID is not pending or HITL is not configured.
141    pub async fn confirm_tool_use(
142        &self,
143        tool_id: &str,
144        approved: bool,
145        reason: Option<String>,
146    ) -> Result<bool> {
147        HitlControl::from_session(self)
148            .confirm_tool_use(tool_id, approved, reason)
149            .await
150    }
151
152    /// Cancel all pending HITL confirmations for this session.
153    pub async fn cancel_confirmations(&self) -> usize {
154        HitlControl::from_session(self).cancel_confirmations().await
155    }
156
157    /// Return structured verification reports recorded for this session.
158    pub fn verification_reports(&self) -> Vec<crate::verification::VerificationReport> {
159        VerificationRuntime::from_session(self).reports()
160    }
161
162    /// Return a structured summary of all verification reports recorded for this session.
163    pub fn verification_summary(&self) -> crate::verification::VerificationSummary {
164        VerificationRuntime::from_session(self).summary()
165    }
166
167    /// Return a concise human-readable verification summary for this session.
168    pub fn verification_summary_text(&self) -> String {
169        VerificationRuntime::from_session(self).summary_text()
170    }
171
172    /// Add externally produced verification reports to this session's completion evidence.
173    pub fn record_verification_reports(
174        &self,
175        reports: impl IntoIterator<Item = crate::verification::VerificationReport>,
176    ) {
177        VerificationRuntime::from_session(self).record(reports);
178    }
179
180    /// Register a hook for lifecycle event interception.
181    pub fn register_hook(&self, hook: crate::hooks::Hook) -> crate::error::Result<()> {
182        self.close_handle.mutate_immediate(|| {
183            self.ensure_compatibility_name_available(
184                crate::capability::CapabilityKind::Hook,
185                &hook.id,
186            )?;
187            HookControl::from_session(self).register_hook(hook);
188            Ok(())
189        })?
190    }
191
192    /// Unregister a hook by ID.
193    pub fn unregister_hook(
194        &self,
195        hook_id: &str,
196    ) -> crate::error::Result<Option<crate::hooks::Hook>> {
197        self.close_handle.mutate_immediate(|| {
198            self.ensure_compatibility_name_available(
199                crate::capability::CapabilityKind::Hook,
200                hook_id,
201            )?;
202            Ok(HookControl::from_session(self).unregister_hook(hook_id))
203        })?
204    }
205
206    /// Register a handler for a specific hook.
207    pub fn register_hook_handler(
208        &self,
209        hook_id: &str,
210        handler: Arc<dyn crate::hooks::HookHandler>,
211    ) -> crate::error::Result<()> {
212        let mut pending_handler = Some(handler);
213        let retired = self.close_handle.mutate_immediate(|| {
214            self.ensure_compatibility_name_available(
215                crate::capability::CapabilityKind::Hook,
216                hook_id,
217            )?;
218            let handler = pending_handler.take().ok_or_else(|| {
219                crate::error::CodeError::Capability(
220                    crate::capability::CapabilityRuntimeError::RuntimeValueInvalid {
221                        kind: crate::capability::CapabilityKind::Hook,
222                        public_name: hook_id.to_owned(),
223                        message: "Hook handler mutation was already consumed".to_owned(),
224                    },
225                )
226            })?;
227            Ok::<_, crate::error::CodeError>(
228                HookControl::from_session(self).register_hook_handler(hook_id, handler),
229            )
230        })??;
231        drop(retired);
232        Ok(())
233    }
234
235    /// Unregister a hook handler by hook ID.
236    pub fn unregister_hook_handler(&self, hook_id: &str) -> crate::error::Result<()> {
237        let retired = self.close_handle.mutate_immediate(|| {
238            self.ensure_compatibility_name_available(
239                crate::capability::CapabilityKind::Hook,
240                hook_id,
241            )?;
242            Ok::<_, crate::error::CodeError>(
243                HookControl::from_session(self).unregister_hook_handler(hook_id),
244            )
245        })??;
246        drop(retired);
247        Ok(())
248    }
249
250    /// Atomically add or replace a complete Hook definition and handler pair.
251    ///
252    /// Official SDK bridges use this method so a newly admitted Run cannot
253    /// observe the definition without its matching callback. Passing `None`
254    /// intentionally registers an event-only Hook with no gating handler.
255    pub fn register_hook_registration(
256        &self,
257        hook: crate::hooks::Hook,
258        handler: Option<Arc<dyn crate::hooks::HookHandler>>,
259    ) -> crate::error::Result<()> {
260        let mut pending_handler = Some(handler);
261        let retired = self.close_handle.mutate_immediate(|| {
262            self.ensure_compatibility_name_available(
263                crate::capability::CapabilityKind::Hook,
264                &hook.id,
265            )?;
266            let handler = pending_handler.take().ok_or_else(|| {
267                crate::error::CodeError::Capability(
268                    crate::capability::CapabilityRuntimeError::RuntimeValueInvalid {
269                        kind: crate::capability::CapabilityKind::Hook,
270                        public_name: hook.id.clone(),
271                        message: "Hook registration was already consumed".to_owned(),
272                    },
273                )
274            })?;
275            Ok::<_, crate::error::CodeError>(
276                HookControl::from_session(self).register_hook_registration(hook, handler),
277            )
278        })??;
279        drop(retired);
280        Ok(())
281    }
282
283    /// Atomically remove a complete Hook definition and handler pair.
284    pub fn unregister_hook_registration(
285        &self,
286        hook_id: &str,
287    ) -> crate::error::Result<Option<crate::hooks::Hook>> {
288        let (retired_hook, retired_handler) = self.close_handle.mutate_immediate(|| {
289            self.ensure_compatibility_name_available(
290                crate::capability::CapabilityKind::Hook,
291                hook_id,
292            )?;
293            Ok::<_, crate::error::CodeError>(
294                HookControl::from_session(self).unregister_hook_registration(hook_id),
295            )
296        })??;
297        let removed = retired_hook.as_deref().cloned();
298        drop((retired_hook, retired_handler));
299        Ok(removed)
300    }
301
302    /// Get the number of registered hooks.
303    pub fn hook_count(&self) -> usize {
304        HookControl::from_session(self).hook_count()
305    }
306
307    /// Run verification commands through the session's tool execution path.
308    pub async fn verify_commands(
309        &self,
310        subject: &str,
311        commands: &[crate::verification::VerificationCommand],
312    ) -> Result<crate::verification::VerificationReport> {
313        VerificationRuntime::from_session(self)
314            .verify_commands(subject, commands)
315            .await
316    }
317
318    /// Return project-aware verification command presets for this workspace.
319    pub fn verification_presets(&self) -> Vec<crate::verification::VerificationPreset> {
320        VerificationRuntime::from_session(self).presets()
321    }
322}