car_multi/shared.rs
1//! Shared infrastructure — creates Runtimes that share state, log, and policies.
2
3use crate::budget::{BudgetError, BudgetLimits, CoordinationBudget};
4use crate::types::AgentOutput;
5use car_engine::Runtime;
6use car_eventlog::EventLog;
7use car_policy::PolicyEngine;
8use car_state::StateStore;
9use std::sync::Arc;
10use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock};
11
12/// Factory for creating Runtime instances with shared state, event log, and policies.
13///
14/// In a multi-agent system, all agents see the same state store and write to the
15/// same event log. Each agent gets its own tool set and executor.
16///
17/// A [`CoordinationBudget`] is always present (unbounded by default). Patterns
18/// gate each spawn through [`begin_agent`](Self::begin_agent) and report spend
19/// through [`record_output`](Self::record_output), so budget enforcement is a
20/// uniform, always-on code path that costs nothing when no limits are set.
21pub struct SharedInfra {
22 pub state: Arc<StateStore>,
23 pub log: Arc<TokioMutex<EventLog>>,
24 pub policies: Arc<TokioRwLock<PolicyEngine>>,
25 pub budget: Arc<CoordinationBudget>,
26}
27
28impl SharedInfra {
29 pub fn new() -> Self {
30 Self {
31 state: Arc::new(StateStore::new()),
32 log: Arc::new(TokioMutex::new(EventLog::new())),
33 policies: Arc::new(TokioRwLock::new(PolicyEngine::new())),
34 budget: Arc::new(CoordinationBudget::unbounded()),
35 }
36 }
37
38 /// Attach a coordination budget built from the given limits. Patterns run
39 /// against this infra will refuse to start agents once a limit is crossed.
40 pub fn with_budget(mut self, limits: BudgetLimits) -> Self {
41 self.budget = Arc::new(CoordinationBudget::new(limits));
42 self
43 }
44
45 /// Attach a pre-built (possibly shared) coordination budget.
46 pub fn with_shared_budget(mut self, budget: Arc<CoordinationBudget>) -> Self {
47 self.budget = budget;
48 self
49 }
50
51 /// Reserve a budget slot for one agent. `Ok(())` means the agent may run;
52 /// `Err` carries why it was denied. Patterns call this immediately before a
53 /// spawn and record a [`budget_skipped_output`](crate::budget::budget_skipped_output)
54 /// on denial.
55 pub fn begin_agent(&self) -> Result<(), BudgetError> {
56 self.budget.try_begin_agent()
57 }
58
59 /// Record an agent's reported token/cost spend against the budget.
60 pub fn record_output(&self, out: &AgentOutput) {
61 self.budget.record_output(out);
62 }
63
64 /// Create a Runtime that shares this infra's state, log, and policies.
65 ///
66 /// Each runtime gets its own tool set, executor, and idempotency cache.
67 pub fn make_runtime(&self) -> Runtime {
68 Runtime::with_shared(
69 Arc::clone(&self.state),
70 Arc::clone(&self.log),
71 Arc::clone(&self.policies),
72 )
73 }
74
75 /// Create a Runtime with per-agent isolated state overlay.
76 /// Writes go to a local StateStore; reads fall through to shared state.
77 /// Call `AgentContext::merge_to_parent()` after the agent completes.
78 pub fn make_isolated_runtime(
79 &self,
80 agent_name: &str,
81 ) -> (Runtime, crate::task_context::AgentContext) {
82 let ctx = crate::task_context::AgentContext::new(agent_name, Arc::clone(&self.state));
83 let rt = Runtime::with_shared(
84 Arc::clone(&ctx.local_state),
85 Arc::clone(&ctx.local_log),
86 Arc::clone(&self.policies),
87 );
88 (rt, ctx)
89 }
90}
91
92impl Default for SharedInfra {
93 fn default() -> Self {
94 Self::new()
95 }
96}