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    /// Record a kept, reverted, or rejected constraint against a change-set
34    /// digest. The next agent-loop build serves only Accept records. A
35    /// secret-shaped constraint is not stored.
36    pub fn record_outcome(
37        &self,
38        outcome: crate::outcome_memory::OutcomeKind,
39        change_digest: &str,
40        constraint: &str,
41    ) -> crate::error::Result<bool> {
42        self.close_handle.mutate_immediate(|| {
43            let mut slot = self
44                .runtime_outcome_ledger
45                .lock()
46                .unwrap_or_else(|poisoned| poisoned.into_inner());
47            let mut ledger = slot
48                .clone()
49                .unwrap_or_else(|| self.config.outcome_ledger.clone());
50            let stored = match outcome {
51                crate::outcome_memory::OutcomeKind::Accept => {
52                    ledger.accept(change_digest, constraint)
53                }
54                crate::outcome_memory::OutcomeKind::Revert => {
55                    ledger.revert(change_digest, constraint)
56                }
57                crate::outcome_memory::OutcomeKind::Reject => {
58                    ledger.reject(change_digest, constraint)
59                }
60            };
61            if stored {
62                *slot = Some(ledger);
63            }
64            stored
65        })
66    }
67
68    /// Remember the promoted isolation digest without activating recall.
69    pub fn note_promoted_digest(&self, digest: &str) -> crate::error::Result<bool> {
70        self.close_handle.mutate_immediate(|| {
71            let mut slot = self
72                .runtime_outcome_ledger
73                .lock()
74                .unwrap_or_else(|poisoned| poisoned.into_inner());
75            let mut ledger = slot
76                .clone()
77                .unwrap_or_else(|| self.config.outcome_ledger.clone());
78            let stored = ledger.note_promoted(digest);
79            if stored {
80                *slot = Some(ledger);
81            }
82            stored
83        })
84    }
85
86    /// Ledger the next turn will serve, including host records made since
87    /// session construction.
88    pub fn outcome_ledger_snapshot(&self) -> crate::outcome_memory::OutcomeLedger {
89        self.runtime_outcome_ledger
90            .lock()
91            .unwrap_or_else(|poisoned| poisoned.into_inner())
92            .clone()
93            .unwrap_or_else(|| self.config.outcome_ledger.clone())
94    }
95
96    pub(crate) fn outcome_ledger_override(&self) -> Option<crate::outcome_memory::OutcomeLedger> {
97        self.runtime_outcome_ledger
98            .lock()
99            .unwrap_or_else(|poisoned| poisoned.into_inner())
100            .clone()
101    }
102
103    /// Return the currently-installed runtime budget guard, if any.
104    /// `None` means the loop falls back to `config.budget_guard`.
105    pub fn budget_guard(&self) -> Option<Arc<dyn crate::budget::BudgetGuard>> {
106        self.runtime_budget_guard
107            .lock()
108            .unwrap_or_else(|p| p.into_inner())
109            .clone()
110    }
111
112    /// Override specialty prompt style for subsequent turns without rebuilding
113    /// the session. Pass `Some(AgentStyle::Plan)` for Plan; pass `None` to clear
114    /// specialty style back to GeneralPurpose. Takes effect on the next
115    /// `send` / `stream` via agent-loop build (`PROMPT-ALIGN1` hot path).
116    pub fn set_agent_style(
117        &self,
118        style: Option<crate::prompts::AgentStyle>,
119    ) -> crate::error::Result<()> {
120        self.close_handle.mutate_immediate(|| {
121            let mut slot = self
122                .runtime_agent_style
123                .lock()
124                .unwrap_or_else(|p| p.into_inner());
125            *slot = Some(style);
126            Ok(())
127        })?
128    }
129
130    /// Override planning mode for subsequent turns without rebuilding the
131    /// session. Durable `/goal` hosts use this to keep maker planning Enabled
132    /// while forcing verifier turns onto Disabled so Flash cannot stall in a
133    /// plan-only wave. Takes effect on the next `send` / `stream`.
134    pub fn set_planning_mode(
135        &self,
136        mode: crate::prompts::PlanningMode,
137    ) -> crate::error::Result<()> {
138        self.close_handle.mutate_immediate(|| {
139            let mut slot = self
140                .runtime_planning_mode
141                .lock()
142                .unwrap_or_else(|p| p.into_inner());
143            *slot = Some(mode);
144            Ok(())
145        })?
146    }
147
148    /// Clear a prior [`Self::set_planning_mode`] override so the next loop
149    /// uses the session-built `config.planning_mode` again.
150    pub fn clear_planning_mode_override(&self) -> crate::error::Result<()> {
151        self.close_handle.mutate_immediate(|| {
152            let mut slot = self
153                .runtime_planning_mode
154                .lock()
155                .unwrap_or_else(|p| p.into_inner());
156            *slot = None;
157            Ok(())
158        })?
159    }
160
161    /// Pin or clear the user-facing reply language for subsequent turns without
162    /// rebuilding the session. Pass `Some("zh-CN")` to pin; pass `None` to clear
163    /// the runtime override so `prompt_slots.output_language` applies again.
164    /// Takes effect on the next `send` / `stream`.
165    pub fn set_output_language(
166        &self,
167        language: Option<impl Into<String>>,
168    ) -> crate::error::Result<()> {
169        let language = language.map(Into::into).and_then(|value| {
170            let trimmed = value.trim();
171            (!trimmed.is_empty()).then(|| trimmed.to_string())
172        });
173        self.close_handle.mutate_immediate(|| {
174            let mut slot = self
175                .runtime_output_language
176                .lock()
177                .unwrap_or_else(|p| p.into_inner());
178            *slot = Some(language);
179            Ok(())
180        })?
181    }
182
183    /// Runtime agent-style override. Outer `None` means unset; inner value is
184    /// the style (or cleared specialty when `Some(None)`).
185    pub(crate) fn runtime_agent_style_override(
186        &self,
187    ) -> Option<Option<crate::prompts::AgentStyle>> {
188        *self
189            .runtime_agent_style
190            .lock()
191            .unwrap_or_else(|p| p.into_inner())
192    }
193
194    /// Runtime planning-mode override. `None` means unset.
195    pub(crate) fn runtime_planning_mode_override(&self) -> Option<crate::prompts::PlanningMode> {
196        *self
197            .runtime_planning_mode
198            .lock()
199            .unwrap_or_else(|p| p.into_inner())
200    }
201
202    /// Runtime reply-language override. Outer `None` means unset; inner value
203    /// is the BCP-47 tag (or cleared pin when `Some(None)`).
204    pub(crate) fn runtime_output_language_override(&self) -> Option<Option<String>> {
205        self.runtime_output_language
206            .lock()
207            .unwrap_or_else(|p| p.into_inner())
208            .clone()
209    }
210
211    /// Install or clear the host-owned live checkpoint export sink (SDK-CP1).
212    ///
213    /// Takes effect on the next `send` / `stream` that opens a checkpoint
214    /// channel. Callback-backed SDK hosts use this after session construction
215    /// because value-typed `SessionOptions` cannot carry language callables.
216    pub fn set_session_checkpoint_export_sink(
217        &self,
218        sink: Option<Arc<dyn crate::session_checkpoint::SessionCheckpointExportSink>>,
219    ) -> crate::error::Result<()> {
220        self.close_handle.mutate_immediate(|| {
221            let mut slot = self
222                .runtime_session_checkpoint_export_sink
223                .lock()
224                .unwrap_or_else(|p| p.into_inner());
225            *slot = sink;
226        })
227    }
228
229    /// Return the currently installed live checkpoint export sink, if any.
230    pub fn session_checkpoint_export_sink(
231        &self,
232    ) -> Option<Arc<dyn crate::session_checkpoint::SessionCheckpointExportSink>> {
233        self.runtime_session_checkpoint_export_sink
234            .lock()
235            .unwrap_or_else(|p| p.into_inner())
236            .clone()
237    }
238
239    /// Return pending HITL tool confirmations for this session.
240    pub async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
241        HitlControl::from_session(self)
242            .pending_confirmations()
243            .await
244    }
245
246    /// Resolve a pending HITL tool confirmation.
247    ///
248    /// Returns `Ok(true)` when a pending confirmation was found and completed,
249    /// `Ok(false)` when the tool ID is not pending or HITL is not configured.
250    pub async fn confirm_tool_use(
251        &self,
252        tool_id: &str,
253        approved: bool,
254        reason: Option<String>,
255    ) -> Result<bool> {
256        if let Some(manager) = &self.config.confirmation_manager {
257            if manager
258                .confirm(tool_id, approved, reason.clone())
259                .await
260                .map_err(crate::error::CodeError::Session)?
261            {
262                return Ok(true);
263            }
264        }
265        let run = super::conversation_runtime::FactSession::from(self).open()?;
266        Ok(run.confirm_if_pending(tool_id, approved).await?)
267    }
268
269    /// Cancel all pending HITL confirmations for this session.
270    pub async fn cancel_confirmations(&self) -> usize {
271        HitlControl::from_session(self).cancel_confirmations().await
272    }
273
274    /// Return structured verification reports recorded for this session.
275    pub fn verification_reports(&self) -> Vec<crate::verification::VerificationReport> {
276        VerificationRuntime::from_session(self).reports()
277    }
278
279    /// Return a structured summary of all verification reports recorded for this session.
280    pub fn verification_summary(&self) -> crate::verification::VerificationSummary {
281        VerificationRuntime::from_session(self).summary()
282    }
283
284    /// Return a concise human-readable verification summary for this session.
285    pub fn verification_summary_text(&self) -> String {
286        VerificationRuntime::from_session(self).summary_text()
287    }
288
289    /// Add externally produced verification reports to this session's completion evidence.
290    pub fn record_verification_reports(
291        &self,
292        reports: impl IntoIterator<Item = crate::verification::VerificationReport>,
293    ) {
294        VerificationRuntime::from_session(self).record(reports);
295    }
296
297    /// Register a hook for lifecycle event interception.
298    pub fn register_hook(&self, hook: crate::hooks::Hook) -> crate::error::Result<()> {
299        self.close_handle.mutate_immediate(|| {
300            self.ensure_compatibility_name_available(
301                crate::capability::CapabilityKind::Hook,
302                &hook.id,
303            )?;
304            HookControl::from_session(self).register_hook(hook);
305            Ok(())
306        })?
307    }
308
309    /// Unregister a hook by ID.
310    pub fn unregister_hook(
311        &self,
312        hook_id: &str,
313    ) -> crate::error::Result<Option<crate::hooks::Hook>> {
314        self.close_handle.mutate_immediate(|| {
315            self.ensure_compatibility_name_available(
316                crate::capability::CapabilityKind::Hook,
317                hook_id,
318            )?;
319            Ok(HookControl::from_session(self).unregister_hook(hook_id))
320        })?
321    }
322
323    /// Register a handler for a specific hook.
324    pub fn register_hook_handler(
325        &self,
326        hook_id: &str,
327        handler: Arc<dyn crate::hooks::HookHandler>,
328    ) -> crate::error::Result<()> {
329        let mut pending_handler = Some(handler);
330        let retired = self.close_handle.mutate_immediate(|| {
331            self.ensure_compatibility_name_available(
332                crate::capability::CapabilityKind::Hook,
333                hook_id,
334            )?;
335            let handler = pending_handler.take().ok_or_else(|| {
336                crate::error::CodeError::Capability(
337                    crate::capability::CapabilityRuntimeError::RuntimeValueInvalid {
338                        kind: crate::capability::CapabilityKind::Hook,
339                        public_name: hook_id.to_owned(),
340                        message: "Hook handler mutation was already consumed".to_owned(),
341                    },
342                )
343            })?;
344            Ok::<_, crate::error::CodeError>(
345                HookControl::from_session(self).register_hook_handler(hook_id, handler),
346            )
347        })??;
348        drop(retired);
349        Ok(())
350    }
351
352    /// Unregister a hook handler by hook ID.
353    pub fn unregister_hook_handler(&self, hook_id: &str) -> crate::error::Result<()> {
354        let retired = self.close_handle.mutate_immediate(|| {
355            self.ensure_compatibility_name_available(
356                crate::capability::CapabilityKind::Hook,
357                hook_id,
358            )?;
359            Ok::<_, crate::error::CodeError>(
360                HookControl::from_session(self).unregister_hook_handler(hook_id),
361            )
362        })??;
363        drop(retired);
364        Ok(())
365    }
366
367    /// Atomically add or replace a complete Hook definition and handler pair.
368    ///
369    /// Official SDK bridges use this method so a newly admitted Run cannot
370    /// observe the definition without its matching callback. Passing `None`
371    /// intentionally registers an event-only Hook with no gating handler.
372    pub fn register_hook_registration(
373        &self,
374        hook: crate::hooks::Hook,
375        handler: Option<Arc<dyn crate::hooks::HookHandler>>,
376    ) -> crate::error::Result<()> {
377        let mut pending_handler = Some(handler);
378        let retired = self.close_handle.mutate_immediate(|| {
379            self.ensure_compatibility_name_available(
380                crate::capability::CapabilityKind::Hook,
381                &hook.id,
382            )?;
383            let handler = pending_handler.take().ok_or_else(|| {
384                crate::error::CodeError::Capability(
385                    crate::capability::CapabilityRuntimeError::RuntimeValueInvalid {
386                        kind: crate::capability::CapabilityKind::Hook,
387                        public_name: hook.id.clone(),
388                        message: "Hook registration was already consumed".to_owned(),
389                    },
390                )
391            })?;
392            Ok::<_, crate::error::CodeError>(
393                HookControl::from_session(self).register_hook_registration(hook, handler),
394            )
395        })??;
396        drop(retired);
397        Ok(())
398    }
399
400    /// Atomically remove a complete Hook definition and handler pair.
401    pub fn unregister_hook_registration(
402        &self,
403        hook_id: &str,
404    ) -> crate::error::Result<Option<crate::hooks::Hook>> {
405        let (retired_hook, retired_handler) = self.close_handle.mutate_immediate(|| {
406            self.ensure_compatibility_name_available(
407                crate::capability::CapabilityKind::Hook,
408                hook_id,
409            )?;
410            Ok::<_, crate::error::CodeError>(
411                HookControl::from_session(self).unregister_hook_registration(hook_id),
412            )
413        })??;
414        let removed = retired_hook.as_deref().cloned();
415        drop((retired_hook, retired_handler));
416        Ok(removed)
417    }
418
419    /// Get the number of registered hooks.
420    pub fn hook_count(&self) -> usize {
421        HookControl::from_session(self).hook_count()
422    }
423
424    /// Run verification commands through the session's tool execution path.
425    pub async fn verify_commands(
426        &self,
427        subject: &str,
428        commands: &[crate::verification::VerificationCommand],
429    ) -> Result<crate::verification::VerificationReport> {
430        VerificationRuntime::from_session(self)
431            .verify_commands(subject, commands)
432            .await
433    }
434
435    /// Return project-aware verification command presets for this workspace.
436    pub fn verification_presets(&self) -> Vec<crate::verification::VerificationPreset> {
437        VerificationRuntime::from_session(self).presets()
438    }
439}