harn_vm/orchestration/
mod.rs1use std::path::PathBuf;
2use std::{cell::RefCell, thread_local};
3
4use serde::{Deserialize, Serialize};
5
6use crate::llm::vm_value_to_json;
7use crate::value::{VmError, VmValue};
8
9pub(crate) fn now_unix_seconds_text() -> String {
18 use std::time::{SystemTime, UNIX_EPOCH};
19 let ts = SystemTime::now()
20 .duration_since(UNIX_EPOCH)
21 .unwrap_or_default()
22 .as_secs();
23 format!("{ts}")
24}
25
26pub(crate) fn new_id(prefix: &str) -> String {
27 format!("{prefix}_{}", uuid::Uuid::now_v7())
28}
29
30pub(crate) fn default_run_dir() -> PathBuf {
31 let base = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
32 crate::runtime_paths::run_root(&base)
33}
34
35mod hooks;
36pub use hooks::*;
37#[cfg(test)]
38mod tests_lazy_hooks;
39
40mod pipeline_lifecycle;
41pub use pipeline_lifecycle::*;
42
43mod settlement_agent;
44pub use settlement_agent::*;
45
46mod lifecycle_receipts;
47pub use lifecycle_receipts::*;
48
49mod command_policy;
50pub use command_policy::*;
51
52mod tool_precheck;
53pub use tool_precheck::*;
54
55mod approval_reviewer;
59pub use approval_reviewer::*;
60
61mod compaction;
62pub use compaction::*;
63
64mod repair_ledger;
65
66mod compact_lifecycle;
67pub use compact_lifecycle::*;
68
69mod compaction_receipt;
70pub use compaction_receipt::*;
71
72mod compaction_policy_registry;
73pub use compaction_policy_registry::*;
74
75pub mod agent_inbox;
76
77mod artifacts;
78pub use artifacts::*;
79
80mod assemble;
81pub use assemble::*;
82
83mod handoffs;
84pub use handoffs::*;
85
86mod friction;
87pub use friction::*;
88
89mod crystallize;
90pub use crystallize::*;
91
92mod release_fixture;
93pub use release_fixture::*;
94
95mod replay_oracle;
96pub use replay_oracle::*;
97
98mod replay_bench;
99pub use replay_bench::*;
100
101mod policy;
102pub use policy::*;
103#[cfg(test)]
104pub(crate) use policy::{is_policy_machinery_consent_call, swap_execution_policy_stack};
105
106mod ambient_scope;
107pub use ambient_scope::blocking::run_blocking_with_ambient;
108pub(crate) use ambient_scope::{
109 scope_agent_session, scope_ambient, scope_ambient_transaction, scope_approval_policy,
110 scope_autonomy_policy, scope_command_policy, scope_dynamic_permissions, scope_inline_subtask,
111 scope_run_event_sink, scope_spawned_source_dir, AmbientExecutionScope,
112 RegisteredExecutionPolicy,
113};
114pub use ambient_scope::{
115 scope_ambient_context, scope_execution_policy, scope_fresh_run_runtime,
116 scope_fresh_tracing_runtime, scope_fresh_trigger_registry, scope_llm_runtime_overrides,
117 scope_llm_runtime_overrides_with_provider_endpoints,
118};
119
120mod stage_options;
121pub use stage_options::*;
122
123mod workflow;
124pub use workflow::*;
125
126mod workflow_bundle;
127pub use workflow_bundle::*;
128
129mod workflow_patch;
130pub use workflow_patch::*;
131
132mod safe_function_tools;
133pub use safe_function_tools::*;
134
135mod nested_invocation;
136pub use nested_invocation::*;
137
138#[cfg(test)]
139mod workflow_test_fixtures;
140
141mod records;
142pub use records::*;
143
144mod run_review;
145pub use run_review::*;
146
147mod run_view_fixtures;
148pub use run_view_fixtures::*;
149
150mod training_example;
151pub use training_example::*;
152
153mod context_eval;
154pub use context_eval::*;
155
156mod skill_gate;
157pub use skill_gate::*;
158
159mod merge_captain_audit;
160pub use merge_captain_audit::*;
161
162mod merge_captain_driver;
163pub use merge_captain_driver::*;
164
165mod merge_captain_ladder;
166pub use merge_captain_ladder::*;
167
168mod merge_captain_iteration;
169pub use merge_captain_iteration::*;
170
171pub mod playground;
172
173thread_local! {
174 static CURRENT_MUTATION_SESSION: RefCell<Option<MutationSessionRecord>> = const { RefCell::new(None) };
175 static CURRENT_WORKFLOW_STAGE_CONTEXT: RefCell<Option<WorkflowStageContext>> = const { RefCell::new(None) };
181}
182
183#[derive(Clone, Default)]
192pub struct WorkflowStageContext {
193 pub registry: Option<VmValue>,
194 pub match_config: Option<VmValue>,
195 pub tool_search: Option<VmValue>,
198}
199
200impl WorkflowStageContext {
201 pub fn apply_current_to_stage_config(config: &mut crate::value::DictMap) {
207 let Some(context) = current_workflow_stage_context() else {
208 return;
209 };
210 for (key, value) in [
211 ("skills", context.registry),
212 ("skill_match", context.match_config),
213 ("tool_search", context.tool_search),
214 ] {
215 if let Some(value) = value {
216 config.insert(crate::value::intern_key(key), value);
217 }
218 }
219 }
220}
221
222pub fn install_workflow_stage_context(context: Option<WorkflowStageContext>) {
223 CURRENT_WORKFLOW_STAGE_CONTEXT.with(|slot| {
224 *slot.borrow_mut() = context;
225 });
226}
227
228pub fn current_workflow_stage_context() -> Option<WorkflowStageContext> {
229 CURRENT_WORKFLOW_STAGE_CONTEXT.with(|slot| slot.borrow().clone())
230}
231
232pub struct WorkflowStageContextGuard;
236
237impl Drop for WorkflowStageContextGuard {
238 fn drop(&mut self) {
239 install_workflow_stage_context(None);
240 }
241}
242
243#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
244#[serde(default)]
245pub struct MutationSessionRecord {
246 pub session_id: String,
247 pub parent_session_id: Option<String>,
248 pub run_id: Option<String>,
249 pub worker_id: Option<String>,
250 pub execution_kind: Option<String>,
251 pub mutation_scope: String,
252 pub approval_policy: Option<ToolApprovalPolicy>,
256}
257
258impl MutationSessionRecord {
259 pub fn normalize(mut self) -> Self {
260 if self.session_id.is_empty() {
261 self.session_id = new_id("session");
262 }
263 if self.mutation_scope.is_empty() {
264 self.mutation_scope = "read_only".to_string();
265 }
266 self
267 }
268}
269
270pub fn install_current_mutation_session(session: Option<MutationSessionRecord>) {
271 CURRENT_MUTATION_SESSION.with(|slot| {
272 *slot.borrow_mut() = session.map(MutationSessionRecord::normalize);
273 });
274}
275
276pub fn current_mutation_session() -> Option<MutationSessionRecord> {
277 CURRENT_MUTATION_SESSION.with(|slot| slot.borrow().clone())
278}
279
280pub(crate) fn swap_mutation_session(
288 next: Option<MutationSessionRecord>,
289) -> Option<MutationSessionRecord> {
290 CURRENT_MUTATION_SESSION.with(|slot| std::mem::replace(&mut *slot.borrow_mut(), next))
291}
292
293const PAYLOAD_SNIPPET_MAX_BYTES: usize = 600;
295
296pub(crate) fn parse_json_payload<T: for<'de> Deserialize<'de>>(
297 json: serde_json::Value,
298 label: &str,
299) -> Result<T, VmError> {
300 let payload = json.to_string();
301 let mut deserializer = serde_json::Deserializer::from_str(&payload);
302 let mut tracker = serde_path_to_error::Track::new();
303 let path_deserializer = serde_path_to_error::Deserializer::new(&mut deserializer, &mut tracker);
304 T::deserialize(path_deserializer).map_err(|error| {
305 let snippet = crate::text::truncate_end_bytes(&payload, PAYLOAD_SNIPPET_MAX_BYTES);
306 VmError::Runtime(format!(
307 "{label} parse error at {}: {} | payload={}",
308 tracker.path(),
309 error,
310 snippet
311 ))
312 })
313}
314
315pub(crate) fn parse_json_value<T: for<'de> Deserialize<'de>>(
316 value: &VmValue,
317) -> Result<T, VmError> {
318 parse_json_payload(vm_value_to_json(value), "orchestration")
319}
320
321#[cfg(test)]
322mod tests;
323
324#[cfg(test)]
325mod policy_restriction_tests;
326
327#[cfg(test)]
328mod typed_options_parity;