1use std::collections::{HashMap, VecDeque};
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::sync::{Arc, Weak};
4use std::time::{Duration, Instant};
5
6use async_trait::async_trait;
7use futures::future::join_all;
8use futures::{FutureExt, StreamExt};
9use otherone_ai::error::AiError;
10use otherone_ai::traits::ChatStream;
11use otherone_ai::types::{FunctionDefinition, Message, MessageContent, Tool, ToolCall};
12use otherone_storage::types::{AttributeBag, StorageType as StorageBackend, WriteEntryOptions};
13use otherone_tools::types::{
14 ToolCallContext, ToolError, ToolErrorKind, ToolExecutionClass, ToolResult,
15};
16use otherone_tools::{ToolHandler, ToolRegistration, ToolRegistry};
17use serde::Deserialize;
18use tokio::sync::{mpsc, oneshot, Mutex, Notify};
19use tokio_util::sync::CancellationToken;
20
21use crate::error::AgentError;
22use crate::types::{ContextLoadType, StorageType};
23
24use super::event::EventBus;
25use super::registry::{
26 validate_snapshot, AgentRegistry, ModelRegistry, RuntimePolicy, RuntimeSnapshot,
27 SkillRegistrySnapshot, AGENT_CALL_TOOL_NAME, MEMORY_RECALL_TOOL_NAME, MEMORY_STORE_TOOL_NAME,
28};
29use super::supervisor::{BudgetLedger, InMemoryRunStore, RunContext, RunSupervisor, SessionLease};
30use super::types::{
31 merge_other, metadata_with_lineage, AgentCallId, AgentCallRequest, AgentCallResult,
32 AgentDefinition, AgentEvent, AgentFailure, AgentId, AgentInput, AgentOutput, AgentRunCommand,
33 AgentRunContextView, AgentRunRequest, AgentRunResult, EffectiveLimits, MemoryPolicy,
34 ModelProfile, ModelProfileId, ResultContract, RunId, RunOutcome, RunRecord, RunStatus,
35 RunUsage, RuntimeLimits,
36};
37
38const EVENT_CHANNEL_CAPACITY: usize = 512;
39const COMMAND_CHANNEL_CAPACITY: usize = 64;
40const ROOT_IDEMPOTENCY_CACHE_CAPACITY: usize = 1024;
41
42#[async_trait]
43pub trait ModelExecutor: Send + Sync {
44 async fn stream(
45 &self,
46 profile: &ModelProfile,
47 config: serde_json::Value,
48 ) -> Result<ChatStream, AiError>;
49}
50
51#[derive(Default)]
52pub struct DefaultModelExecutor;
53
54#[async_trait]
55impl ModelExecutor for DefaultModelExecutor {
56 async fn stream(
57 &self,
58 profile: &ModelProfile,
59 config: serde_json::Value,
60 ) -> Result<ChatStream, AiError> {
61 otherone_ai::invoke_model_stream(
62 profile.provider.clone(),
63 profile.api_key(),
64 &profile.base_url,
65 config,
66 )
67 .await
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum AuthorizationDecision {
73 Allow,
74 Deny(String),
75}
76
77#[async_trait]
78pub trait AgentCallAuthorizer: Send + Sync {
79 async fn authorize(
80 &self,
81 caller: &AgentRunContextView,
82 target: &AgentDefinition,
83 request: &AgentCallRequest,
84 ) -> Result<AuthorizationDecision, AgentError>;
85}
86
87#[derive(Default)]
88struct AllowAllAuthorizer;
89
90#[async_trait]
91impl AgentCallAuthorizer for AllowAllAuthorizer {
92 async fn authorize(
93 &self,
94 _caller: &AgentRunContextView,
95 _target: &AgentDefinition,
96 _request: &AgentCallRequest,
97 ) -> Result<AuthorizationDecision, AgentError> {
98 Ok(AuthorizationDecision::Allow)
99 }
100}
101
102pub struct AgentRuntimeBuilder {
103 agents: Vec<AgentDefinition>,
104 models: Vec<ModelProfile>,
105 tools: Vec<ToolRegistration>,
106 skills: SkillRegistrySnapshot,
107 default_model: Option<ModelProfileId>,
108 policy: RuntimePolicy,
109 limits: RuntimeLimits,
110 model_executor: Arc<dyn ModelExecutor>,
111 authorizer: Arc<dyn AgentCallAuthorizer>,
112}
113
114impl Default for AgentRuntimeBuilder {
115 fn default() -> Self {
116 Self {
117 agents: Vec::new(),
118 models: Vec::new(),
119 tools: Vec::new(),
120 skills: SkillRegistrySnapshot::default(),
121 default_model: None,
122 policy: RuntimePolicy::default(),
123 limits: RuntimeLimits::default(),
124 model_executor: Arc::new(DefaultModelExecutor),
125 authorizer: Arc::new(AllowAllAuthorizer),
126 }
127 }
128}
129
130impl AgentRuntimeBuilder {
131 pub fn register_agent(mut self, definition: AgentDefinition) -> Self {
132 self.agents.push(definition);
133 self
134 }
135
136 pub fn register_model(mut self, profile: ModelProfile) -> Self {
137 self.models.push(profile);
138 self
139 }
140
141 pub fn register_tool(mut self, registration: ToolRegistration) -> Self {
142 self.tools.push(registration);
143 self
144 }
145
146 pub fn skills(mut self, registry: &otherone_skills::SkillRegistry) -> Self {
147 self.skills = SkillRegistrySnapshot::from_registry(registry);
148 self
149 }
150
151 pub fn default_model(mut self, id: impl Into<String>) -> Self {
152 self.default_model = Some(ModelProfileId::unchecked(id));
153 self
154 }
155
156 pub fn policy(mut self, policy: RuntimePolicy) -> Self {
157 self.policy = policy;
158 self
159 }
160
161 pub fn runtime_limits(mut self, limits: RuntimeLimits) -> Self {
162 self.limits = limits;
163 self
164 }
165
166 pub fn model_executor(mut self, executor: Arc<dyn ModelExecutor>) -> Self {
167 self.model_executor = executor;
168 self
169 }
170
171 pub fn authorizer(mut self, authorizer: Arc<dyn AgentCallAuthorizer>) -> Self {
172 self.authorizer = authorizer;
173 self
174 }
175
176 pub fn build(self) -> Result<AgentRuntime, AgentError> {
177 self.limits.validate()?;
178
179 let mut agents = AgentRegistry::new();
180 for definition in self.agents {
181 agents.register(definition)?;
182 }
183
184 let mut models = ModelRegistry::new();
185 for profile in self.models {
186 models.register(profile)?;
187 }
188
189 let default_model = match self.default_model {
190 Some(id) => {
191 id.validate()?;
192 id
193 }
194 None => models.ids().into_iter().next().ok_or_else(|| {
195 AgentError::InvalidConfiguration(
196 "at least one model profile must be registered".to_string(),
197 )
198 })?,
199 };
200
201 let mut user_tools = ToolRegistry::new();
202 for registration in self.tools {
203 if matches!(
204 registration.name(),
205 AGENT_CALL_TOOL_NAME | MEMORY_RECALL_TOOL_NAME | MEMORY_STORE_TOOL_NAME
206 ) {
207 return Err(AgentError::InvalidConfiguration(format!(
208 "tool name '{}' is reserved by Otherone",
209 registration.name()
210 )));
211 }
212 user_tools
213 .register(registration)
214 .map_err(|error| AgentError::InvalidConfiguration(error.to_string()))?;
215 }
216
217 let agent_snapshot = Arc::new(agents.snapshot());
218 let model_snapshot = Arc::new(models.snapshot());
219 let skill_snapshot = Arc::new(self.skills);
220 let policy = Arc::new(self.policy);
221 let limits = self.limits;
222 let model_executor = self.model_executor;
223 let authorizer = self.authorizer;
224
225 let inner = Arc::new_cyclic(|weak| {
226 let mut tools = user_tools.clone();
227 tools
228 .register(agent_call_registration(weak.clone()))
229 .expect("reserved agent call tool must register");
230
231 let memory_lock = global_memory_lock();
232 tools
233 .register(memory_recall_registration(Arc::clone(&memory_lock)))
234 .expect("reserved memory recall tool must register");
235 tools
236 .register(memory_store_registration(memory_lock))
237 .expect("reserved memory store tool must register");
238
239 let snapshot = Arc::new(RuntimeSnapshot {
240 version: uuid::Uuid::new_v4().to_string(),
241 default_model: default_model.clone(),
242 agents: Arc::clone(&agent_snapshot),
243 models: Arc::clone(&model_snapshot),
244 tools: Arc::new(tools),
245 skills: Arc::clone(&skill_snapshot),
246 policy: Arc::clone(&policy),
247 });
248
249 RuntimeInner {
250 snapshot,
251 supervisor: RunSupervisor::new(&limits),
252 limits: limits.clone(),
253 model_executor: Arc::clone(&model_executor),
254 authorizer: Arc::clone(&authorizer),
255 call_slots: Mutex::new(HashMap::new()),
256 root_slots: Mutex::new(HashMap::new()),
257 }
258 });
259
260 validate_snapshot(&inner.snapshot)?;
261 Ok(AgentRuntime { inner })
262 }
263}
264
265#[derive(Clone)]
266pub struct AgentRuntime {
267 inner: Arc<RuntimeInner>,
268}
269
270impl AgentRuntime {
271 pub fn builder() -> AgentRuntimeBuilder {
272 AgentRuntimeBuilder::default()
273 }
274
275 pub fn snapshot(&self) -> Arc<RuntimeSnapshot> {
276 Arc::clone(&self.inner.snapshot)
277 }
278
279 pub fn run_store(&self) -> InMemoryRunStore {
280 self.inner.supervisor.run_store.clone()
281 }
282
283 pub async fn run(&self, mut request: AgentRunRequest) -> Result<AgentRunResult, AgentError> {
284 let Some(idempotency_key) = request.idempotency_key.take() else {
285 return self.start_internal(request).await?.result().await;
286 };
287 if idempotency_key.is_empty() || idempotency_key.len() > 256 {
288 return Err(AgentError::InvalidConfiguration(
289 "idempotency_key must contain 1-256 characters".to_string(),
290 ));
291 }
292 validate_root_request(&request)?;
293 let key = RootCallKey {
294 partition_key: request
295 .runtime_context
296 .as_ref()
297 .map(|context| context.partition_key.clone())
298 .unwrap_or_else(|| "local".to_string()),
299 idempotency_key,
300 };
301 let (slot, should_start) = {
302 let mut slots = self.inner.root_slots.lock().await;
303 if !slots.contains_key(&key) && slots.len() >= ROOT_IDEMPOTENCY_CACHE_CAPACITY {
304 if let Some(completed_key) = slots
305 .iter()
306 .find_map(|(key, slot)| slot.is_complete().then(|| key.clone()))
307 {
308 slots.remove(&completed_key);
309 } else {
310 return Err(AgentError::BudgetExceeded(
311 "root_idempotency_cache_capacity".to_string(),
312 ));
313 }
314 }
315 match slots.entry(key) {
316 std::collections::hash_map::Entry::Occupied(entry) => {
317 (Arc::clone(entry.get()), false)
318 }
319 std::collections::hash_map::Entry::Vacant(entry) => {
320 let slot = Arc::new(CompletionSlot::new());
321 entry.insert(Arc::clone(&slot));
322 (slot, true)
323 }
324 }
325 };
326 if should_start {
327 let runtime = self.clone();
328 let producer_slot = Arc::clone(&slot);
329 tokio::spawn(async move {
330 let result = match runtime.start_internal(request).await {
331 Ok(handle) => handle.result().await,
332 Err(error) => Err(error),
333 }
334 .map_err(SharedAgentError::from_error);
335 producer_slot.complete(result);
336 });
337 }
338 slot.wait().await.map_err(SharedAgentError::into_error)
339 }
340
341 pub async fn start(&self, request: AgentRunRequest) -> Result<AgentRunHandle, AgentError> {
342 if request.idempotency_key.is_some() {
343 return Err(AgentError::InvalidConfiguration(
344 "streaming start does not replay idempotent event streams; use run() for root idempotency"
345 .to_string(),
346 ));
347 }
348 self.start_internal(request).await
349 }
350
351 async fn start_internal(&self, request: AgentRunRequest) -> Result<AgentRunHandle, AgentError> {
352 validate_root_request(&request)?;
353 let agent = self.inner.snapshot.agent(&request.entry_agent)?;
354 let model = self.inner.snapshot.resolve_model(&agent, None)?;
355 let effective_limits =
356 EffectiveLimits::from_request(&self.inner.limits, request.limits.as_ref());
357 effective_limits.runtime.validate()?;
358 let budget = Arc::new(BudgetLedger::new(effective_limits));
359 budget.reserve_run()?;
360
361 let run_id = RunId::new();
362 let root_run_id = run_id.clone();
363 let session_id = request
364 .session
365 .session_id
366 .clone()
367 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
368 let cancellation = CancellationToken::new();
369 let (event_tx, event_rx) = mpsc::channel(EVENT_CHANNEL_CAPACITY);
370 let events = EventBus::new(
371 event_tx,
372 request.event_visibility,
373 request.include_thinking_events,
374 );
375 let (command_tx, command_rx) = mpsc::channel(COMMAND_CHANNEL_CAPACITY);
376 let metadata =
377 metadata_with_lineage(&request.metadata, &root_run_id, None, None, &agent.id, 0);
378 let deadline = run_deadline(
379 Instant::now(),
380 budget.limits().runtime.root_timeout,
381 budget.limits().runtime.run_timeout,
382 agent.limits.timeout_millis,
383 None,
384 );
385 let mut session = request.session.clone();
386 session.session_id = Some(session_id.clone());
387
388 let context = RunContext {
389 run_id: run_id.clone(),
390 root_run_id: root_run_id.clone(),
391 parent_run_id: None,
392 agent_call_id: None,
393 agent: Arc::clone(&agent),
394 model,
395 session_id: session_id.clone(),
396 parent_session_id: None,
397 session,
398 runtime_context: request.runtime_context.clone(),
399 depth: 0,
400 ancestry: vec![agent.id.clone()],
401 deadline,
402 max_iterations_override: None,
403 result_contract: None,
404 cancellation: cancellation.clone(),
405 budget,
406 snapshot: Arc::clone(&self.inner.snapshot),
407 events,
408 child_count: Arc::new(AtomicUsize::new(0)),
409 active_children: Arc::new(AtomicUsize::new(0)),
410 usage: Arc::new(std::sync::Mutex::new(RunUsage::default())),
411 metadata,
412 };
413
414 let lease = self
415 .inner
416 .supervisor
417 .register(context.clone(), command_tx)
418 .await?;
419 if let Err(error) = self.inner.create_run_record(&context).await {
420 self.inner.supervisor.unregister(&run_id).await;
421 drop(lease);
422 return Err(error);
423 }
424
425 let (result_tx, result_rx) = oneshot::channel();
426 let inner = Arc::clone(&self.inner);
427 tokio::spawn(async move {
428 let result = inner
429 .execute_run(context, request.input, command_rx, lease)
430 .await;
431 let _ = result_tx.send(result);
432 });
433
434 Ok(AgentRunHandle {
435 run_id: run_id.clone(),
436 events: event_rx,
437 commands: AgentRunCommandSender {
438 runtime: self.clone(),
439 run_id,
440 },
441 result: result_rx,
442 })
443 }
444
445 pub async fn cancel(&self, run_id: &RunId) -> Result<(), AgentError> {
446 self.inner.supervisor.cancel(run_id).await
447 }
448
449 pub async fn send_message(
450 &self,
451 run_id: &RunId,
452 messages: Vec<String>,
453 ) -> Result<(), AgentError> {
454 self.inner.supervisor.send_message(run_id, messages).await
455 }
456
457 pub async fn send_command(
458 &self,
459 run_id: &RunId,
460 command: AgentRunCommand,
461 ) -> Result<(), AgentError> {
462 match command {
463 AgentRunCommand::EnqueueUserMessages(messages) => {
464 self.send_message(run_id, messages).await
465 }
466 AgentRunCommand::Cancel => self.cancel(run_id).await,
467 }
468 }
469
470 pub async fn get_run(&self, run_id: &RunId) -> Option<RunRecord> {
471 self.inner.supervisor.run_store.get(run_id).await
472 }
473
474 pub async fn list_run_tree(&self, root_run_id: &RunId) -> Vec<RunRecord> {
475 self.inner.supervisor.run_store.list_root(root_run_id).await
476 }
477
478 pub async fn call_agent(
479 &self,
480 caller_run_id: &RunId,
481 call_id: AgentCallId,
482 request: AgentCallRequest,
483 ) -> Result<AgentCallResult, AgentError> {
484 RuntimeInner::call_agent(Arc::clone(&self.inner), caller_run_id, call_id, request).await
485 }
486}
487
488pub struct AgentRunHandle {
489 pub run_id: RunId,
490 pub events: mpsc::Receiver<super::types::AgentEventEnvelope>,
491 pub commands: AgentRunCommandSender,
492 result: oneshot::Receiver<AgentRunResult>,
493}
494
495impl AgentRunHandle {
496 pub async fn result(self) -> Result<AgentRunResult, AgentError> {
497 self.result
498 .await
499 .map_err(|_| AgentError::Internal("Agent run task stopped unexpectedly".to_string()))
500 }
501}
502
503#[derive(Clone)]
504pub struct AgentRunCommandSender {
505 runtime: AgentRuntime,
506 run_id: RunId,
507}
508
509impl AgentRunCommandSender {
510 pub async fn enqueue(&self, messages: Vec<String>) -> Result<(), AgentError> {
511 self.runtime.send_message(&self.run_id, messages).await
512 }
513
514 pub async fn cancel(&self) -> Result<(), AgentError> {
515 self.runtime.cancel(&self.run_id).await
516 }
517}
518
519struct RuntimeInner {
520 snapshot: Arc<RuntimeSnapshot>,
521 supervisor: RunSupervisor,
522 limits: RuntimeLimits,
523 model_executor: Arc<dyn ModelExecutor>,
524 authorizer: Arc<dyn AgentCallAuthorizer>,
525 call_slots: Mutex<HashMap<CallKey, Arc<CompletionSlot<AgentCallResult>>>>,
526 root_slots:
527 Mutex<HashMap<RootCallKey, Arc<CompletionSlot<Result<AgentRunResult, SharedAgentError>>>>>,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Hash)]
531struct CallKey {
532 root_run_id: RunId,
533 caller_run_id: RunId,
534 call_id: AgentCallId,
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Hash)]
538struct RootCallKey {
539 partition_key: String,
540 idempotency_key: String,
541}
542
543struct CompletionSlot<T> {
544 value: std::sync::OnceLock<T>,
545 notify: Notify,
546}
547
548impl<T> CompletionSlot<T>
549where
550 T: Clone,
551{
552 fn new() -> Self {
553 Self {
554 value: std::sync::OnceLock::new(),
555 notify: Notify::new(),
556 }
557 }
558
559 fn is_complete(&self) -> bool {
560 self.value.get().is_some()
561 }
562
563 fn complete(&self, value: T) {
564 let _ = self.value.set(value);
565 self.notify.notify_waiters();
566 }
567
568 async fn wait(&self) -> T {
569 loop {
570 let notified = self.notify.notified();
571 if let Some(value) = self.value.get() {
572 return value.clone();
573 }
574 notified.await;
575 }
576 }
577}
578
579#[derive(Clone)]
580enum SharedAgentError {
581 InvalidConfiguration(String),
582 AgentNotFound(String),
583 AgentCallDenied { caller: String, target: String },
584 AgentCallCycle(String),
585 MaxDepthExceeded(usize),
586 BudgetExceeded(String),
587 SessionBusy(String),
588 TimedOut,
589 Cancelled,
590 ContextError(String),
591 ToolError(String),
592 StorageError(String),
593 Internal(String),
594 MaxIterationsExceeded(u32),
595}
596
597impl SharedAgentError {
598 fn from_error(error: AgentError) -> Self {
599 match error {
600 AgentError::InvalidConfiguration(value) => Self::InvalidConfiguration(value),
601 AgentError::AgentNotFound(value) => Self::AgentNotFound(value),
602 AgentError::AgentCallDenied { caller, target } => {
603 Self::AgentCallDenied { caller, target }
604 }
605 AgentError::AgentCallCycle(value) => Self::AgentCallCycle(value),
606 AgentError::MaxDepthExceeded(value) => Self::MaxDepthExceeded(value),
607 AgentError::BudgetExceeded(value) => Self::BudgetExceeded(value),
608 AgentError::SessionBusy(value) => Self::SessionBusy(value),
609 AgentError::TimedOut => Self::TimedOut,
610 AgentError::Cancelled => Self::Cancelled,
611 AgentError::AiError(value) => Self::Internal(value.to_string()),
612 AgentError::ContextError(value) => Self::ContextError(value),
613 AgentError::ToolError(value) => Self::ToolError(value),
614 AgentError::StorageError(value) => Self::StorageError(value),
615 AgentError::Internal(value) => Self::Internal(value),
616 AgentError::MaxIterationsExceeded(value) => Self::MaxIterationsExceeded(value),
617 }
618 }
619
620 fn into_error(self) -> AgentError {
621 match self {
622 Self::InvalidConfiguration(value) => AgentError::InvalidConfiguration(value),
623 Self::AgentNotFound(value) => AgentError::AgentNotFound(value),
624 Self::AgentCallDenied { caller, target } => {
625 AgentError::AgentCallDenied { caller, target }
626 }
627 Self::AgentCallCycle(value) => AgentError::AgentCallCycle(value),
628 Self::MaxDepthExceeded(value) => AgentError::MaxDepthExceeded(value),
629 Self::BudgetExceeded(value) => AgentError::BudgetExceeded(value),
630 Self::SessionBusy(value) => AgentError::SessionBusy(value),
631 Self::TimedOut => AgentError::TimedOut,
632 Self::Cancelled => AgentError::Cancelled,
633 Self::ContextError(value) => AgentError::ContextError(value),
634 Self::ToolError(value) => AgentError::ToolError(value),
635 Self::StorageError(value) => AgentError::StorageError(value),
636 Self::Internal(value) => AgentError::Internal(value),
637 Self::MaxIterationsExceeded(value) => AgentError::MaxIterationsExceeded(value),
638 }
639 }
640}
641
642impl RuntimeInner {
643 async fn create_run_record(&self, context: &RunContext) -> Result<(), AgentError> {
644 let mut metadata = context.metadata.clone();
645 if let Some(parent_session_id) = &context.parent_session_id {
646 metadata.insert(
647 "parent_session_id".to_string(),
648 serde_json::json!(parent_session_id),
649 );
650 }
651 self.supervisor
652 .run_store
653 .create(RunRecord {
654 run_id: context.run_id.clone(),
655 root_run_id: context.root_run_id.clone(),
656 parent_run_id: context.parent_run_id.clone(),
657 agent_call_id: context.agent_call_id.clone(),
658 agent_id: context.agent.id.clone(),
659 agent_definition_version: context.agent.version,
660 session_id: context.session_id.clone(),
661 status: RunStatus::Created,
662 depth: context.depth,
663 started_at: chrono::Utc::now().to_rfc3339(),
664 finished_at: None,
665 usage: RunUsage::default(),
666 failure: None,
667 metadata,
668 })
669 .await
670 }
671
672 async fn execute_run(
673 self: Arc<Self>,
674 context: RunContext,
675 input: AgentInput,
676 mut command_rx: mpsc::Receiver<AgentRunCommand>,
677 lease: SessionLease,
678 ) -> AgentRunResult {
679 let started = Instant::now();
680 let _ = self
681 .supervisor
682 .run_store
683 .update_status(&context.run_id, RunStatus::Running)
684 .await;
685 emit(&context, AgentEvent::RunStarted);
686
687 let run_loop =
688 std::panic::AssertUnwindSafe(self.run_loop(&context, input, &mut command_rx))
689 .catch_unwind();
690 tokio::pin!(run_loop);
691 let execution = tokio::select! {
692 _ = context.cancellation.cancelled() => Err(AgentError::Cancelled),
693 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(context.deadline)) => {
694 context.cancellation.cancel();
695 Err(AgentError::TimedOut)
696 }
697 result = &mut run_loop => match result {
698 Ok(result) => result,
699 Err(_) => Err(AgentError::Internal("Agent run panicked".to_string())),
700 },
701 };
702
703 let duration = started.elapsed().as_millis().min(u64::MAX as u128) as u64;
704 let (outcome, output, usage, failure, status, terminal_event) = match execution {
705 Ok((output, mut usage)) => {
706 usage.duration_millis = duration;
707 let output = match &context.result_contract {
708 Some(contract) => apply_result_contract(Some(output), contract),
709 None => Ok(Some(output)),
710 };
711 match output {
712 Ok(output) => (
713 RunOutcome::Completed,
714 output,
715 usage,
716 None,
717 RunStatus::Completed,
718 AgentEvent::RunCompleted,
719 ),
720 Err(failure) => (
721 RunOutcome::Failed(failure.clone()),
722 None,
723 usage,
724 Some(failure.clone()),
725 RunStatus::Failed,
726 AgentEvent::RunFailed { failure },
727 ),
728 }
729 }
730 Err(error) => {
731 let (outcome, status, event, failure) = error_outcome(&error);
732 let mut usage = context.usage_snapshot();
733 usage.duration_millis = duration;
734 (outcome, None, usage, failure.clone(), status, event)
735 }
736 };
737
738 if let Some(output) = output.as_ref() {
739 if output.byte_len() > effective_result_limit(&context) {
740 let failure = AgentFailure {
741 code: "max_result_bytes".to_string(),
742 message: "Agent result exceeds the configured size limit".to_string(),
743 retryable: false,
744 };
745 let result = AgentRunResult {
746 run_id: context.run_id.clone(),
747 root_run_id: context.root_run_id.clone(),
748 agent_id: context.agent.id.clone(),
749 session_id: context.session_id.clone(),
750 outcome: RunOutcome::BudgetExceeded,
751 output: None,
752 usage: usage.clone(),
753 artifacts: Vec::new(),
754 metadata: context.metadata.clone(),
755 };
756 let _ = self
757 .supervisor
758 .run_store
759 .finish(
760 &context.run_id,
761 RunStatus::BudgetExceeded,
762 usage,
763 Some(failure.clone()),
764 )
765 .await;
766 emit(&context, AgentEvent::RunFailed { failure });
767 self.supervisor.unregister(&context.run_id).await;
768 if context.depth == 0 {
769 self.call_slots
770 .lock()
771 .await
772 .retain(|key, _| key.root_run_id != context.root_run_id);
773 }
774 drop(lease);
775 return result;
776 }
777 }
778
779 let _ = self
780 .supervisor
781 .run_store
782 .finish(&context.run_id, status, usage.clone(), failure)
783 .await;
784 emit(&context, terminal_event);
785 self.supervisor.unregister(&context.run_id).await;
786 if context.depth == 0 {
787 self.call_slots
788 .lock()
789 .await
790 .retain(|key, _| key.root_run_id != context.root_run_id);
791 }
792 drop(lease);
793
794 AgentRunResult {
795 run_id: context.run_id.clone(),
796 root_run_id: context.root_run_id.clone(),
797 agent_id: context.agent.id.clone(),
798 session_id: context.session_id.clone(),
799 outcome,
800 output,
801 usage,
802 artifacts: Vec::new(),
803 metadata: context.metadata.clone(),
804 }
805 }
806
807 async fn run_loop(
808 &self,
809 context: &RunContext,
810 input: AgentInput,
811 command_rx: &mut mpsc::Receiver<AgentRunCommand>,
812 ) -> Result<(AgentOutput, RunUsage), AgentError> {
813 persist_input(context, input).await?;
814 let mut queued_messages = VecDeque::new();
815 let mut usage = RunUsage::default();
816 let max_iterations = context
817 .agent
818 .limits
819 .max_iterations
820 .unwrap_or(context.budget.limits().runtime.max_iterations_per_run)
821 .min(context.budget.limits().runtime.max_iterations_per_run)
822 .min(
823 context
824 .max_iterations_override
825 .unwrap_or(context.budget.limits().runtime.max_iterations_per_run),
826 );
827 let (tool_registry, tool_definitions) = tool_registry_for(context)?;
828 let system_prompt = system_prompt_for(context);
829
830 for _ in 0..max_iterations {
831 drain_commands(context, command_rx, &mut queued_messages)?;
832 persist_queued_messages(context, &mut queued_messages).await?;
833
834 let messages = combine_context(context, &system_prompt, &tool_definitions).await?;
835 context.budget.reserve_model_call()?;
836 usage.model_calls = usage.model_calls.saturating_add(1);
837 context.record_usage(&usage);
838 emit(context, AgentEvent::ModelStarted);
839
840 let model_config = build_model_config(context, &messages, &tool_definitions);
841 let permit = self
842 .supervisor
843 .model_semaphore
844 .acquire()
845 .await
846 .map_err(|_| AgentError::Internal("model semaphore closed".to_string()))?;
847 let stream = self
848 .model_executor
849 .stream(&context.model, model_config)
850 .await?;
851 let response = collect_model_stream(context, stream).await;
852 drop(permit);
853 let response = response?;
854
855 usage.input_tokens = add_optional(usage.input_tokens, response.input_tokens);
856 usage.output_tokens = add_optional(usage.output_tokens, response.output_tokens);
857 usage.total_tokens = add_optional(usage.total_tokens, response.total_tokens);
858 context.record_usage(&usage);
859 if let Some(tokens) = response.total_tokens {
860 context.budget.add_tokens(tokens)?;
861 }
862 emit(
863 context,
864 AgentEvent::ModelCompleted {
865 usage: response.usage(),
866 },
867 );
868
869 persist_assistant_response(context, &response).await?;
870
871 if response.tool_calls.is_empty() {
872 drain_commands(context, command_rx, &mut queued_messages)?;
873 if !queued_messages.is_empty() {
874 persist_queued_messages(context, &mut queued_messages).await?;
875 continue;
876 }
877 context.budget.add_output_bytes(response.content.len())?;
878 return Ok((AgentOutput::Text(response.content), usage));
879 }
880
881 for _ in &response.tool_calls {
882 context.budget.reserve_tool_call()?;
883 }
884 usage.tool_calls = usage
885 .tool_calls
886 .saturating_add(response.tool_calls.len() as u32);
887 usage.agent_calls = usage.agent_calls.saturating_add(
888 response
889 .tool_calls
890 .iter()
891 .filter(|call| call.function.name == AGENT_CALL_TOOL_NAME)
892 .count() as u32,
893 );
894 context.record_usage(&usage);
895
896 let parallel = context
897 .agent
898 .model_overrides
899 .parallel_tool_calls
900 .unwrap_or(false);
901 let results = self
902 .execute_tools(
903 context,
904 Arc::clone(&tool_registry),
905 &response.tool_calls,
906 parallel,
907 )
908 .await;
909 if serde_json::to_vec(&results)
910 .map(|value| value.len())
911 .unwrap_or(usize::MAX)
912 > effective_result_limit(context)
913 {
914 return Err(AgentError::BudgetExceeded("max_result_bytes".to_string()));
915 }
916 if let Some(result) = results
917 .iter()
918 .find(|result| result.error_kind == Some(ToolErrorKind::Fatal))
919 {
920 return Err(AgentError::ToolError(format!(
921 "tool '{}' failed fatally: {}",
922 result.function_name,
923 result.error.as_deref().unwrap_or("unknown fatal error")
924 )));
925 }
926 persist_tool_results(context, &results).await?;
927
928 drain_commands(context, command_rx, &mut queued_messages)?;
929 persist_queued_messages(context, &mut queued_messages).await?;
930 }
931
932 Err(AgentError::MaxIterationsExceeded(max_iterations))
933 }
934
935 async fn execute_tools(
936 &self,
937 context: &RunContext,
938 registry: Arc<ToolRegistry>,
939 calls: &[ToolCall],
940 parallel: bool,
941 ) -> Vec<ToolResult> {
942 if parallel && calls.len() > 1 {
943 join_all(
944 calls
945 .iter()
946 .cloned()
947 .map(|call| self.execute_tool(context, Arc::clone(®istry), call)),
948 )
949 .await
950 } else {
951 let mut results = Vec::with_capacity(calls.len());
952 for call in calls.iter().cloned() {
953 results.push(
954 self.execute_tool(context, Arc::clone(®istry), call)
955 .await,
956 );
957 }
958 results
959 }
960 }
961
962 async fn execute_tool(
963 &self,
964 context: &RunContext,
965 registry: Arc<ToolRegistry>,
966 call: ToolCall,
967 ) -> ToolResult {
968 emit(
969 context,
970 AgentEvent::ToolStarted {
971 tool_call_id: call.id.clone(),
972 tool_name: call.function.name.clone(),
973 },
974 );
975
976 let execution_class = registry
977 .get(&call.function.name)
978 .map(|registration| registration.execution_class)
979 .unwrap_or(ToolExecutionClass::Standard);
980 let tool_context = ToolCallContext {
981 tool_call_id: call.id.clone(),
982 run_id: context.run_id.to_string(),
983 root_run_id: context.root_run_id.to_string(),
984 agent_id: context.agent.id.to_string(),
985 session_id: Some(context.session_id.clone()),
986 runtime_context: context
987 .runtime_context
988 .as_ref()
989 .and_then(|value| serde_json::to_value(value).ok()),
990 metadata: serde_json::json!({ "memory_policy": context.agent.memory }),
991 deadline: Some(context.deadline),
992 cancellation: context.cancellation.clone(),
993 };
994
995 let result = if execution_class == ToolExecutionClass::Delegating {
996 registry.execute(&call, tool_context).await
997 } else {
998 match self.supervisor.tool_semaphore.acquire().await {
999 Ok(_permit) => registry.execute(&call, tool_context).await,
1000 Err(_) => ToolResult {
1001 tool_call_id: call.id.clone(),
1002 function_name: call.function.name.clone(),
1003 result: None,
1004 error: Some("tool semaphore closed".to_string()),
1005 error_kind: Some(ToolErrorKind::Fatal),
1006 },
1007 }
1008 };
1009
1010 emit(
1011 context,
1012 AgentEvent::ToolCompleted {
1013 tool_call_id: call.id,
1014 tool_name: call.function.name,
1015 error: result.error.clone(),
1016 },
1017 );
1018 result
1019 }
1020
1021 async fn call_agent(
1022 self: Arc<Self>,
1023 caller_run_id: &RunId,
1024 call_id: AgentCallId,
1025 request: AgentCallRequest,
1026 ) -> Result<AgentCallResult, AgentError> {
1027 let caller = self
1028 .supervisor
1029 .active(caller_run_id)
1030 .await
1031 .ok_or_else(|| {
1032 AgentError::InvalidConfiguration(format!(
1033 "caller run '{caller_run_id}' is not active"
1034 ))
1035 })?
1036 .context;
1037 let key = CallKey {
1038 root_run_id: caller.root_run_id.clone(),
1039 caller_run_id: caller.run_id.clone(),
1040 call_id: call_id.clone(),
1041 };
1042 let (slot, should_start) = {
1043 let mut slots = self.call_slots.lock().await;
1044 match slots.entry(key) {
1045 std::collections::hash_map::Entry::Occupied(entry) => {
1046 (Arc::clone(entry.get()), false)
1047 }
1048 std::collections::hash_map::Entry::Vacant(entry) => {
1049 let slot = Arc::new(CompletionSlot::new());
1050 entry.insert(Arc::clone(&slot));
1051 (slot, true)
1052 }
1053 }
1054 };
1055 if should_start {
1056 let producer_slot = Arc::clone(&slot);
1057 tokio::spawn(async move {
1058 let result = self.execute_agent_call(caller, call_id, request).await;
1059 producer_slot.complete(result);
1060 });
1061 }
1062 Ok(slot.wait().await)
1063 }
1064
1065 async fn execute_agent_call(
1066 self: Arc<Self>,
1067 caller: RunContext,
1068 call_id: AgentCallId,
1069 request: AgentCallRequest,
1070 ) -> AgentCallResult {
1071 emit(
1072 &caller,
1073 AgentEvent::AgentCallStarted {
1074 call_id: call_id.clone(),
1075 target: request.target.clone(),
1076 },
1077 );
1078 let execution = {
1079 let execution = self.try_execute_agent_call(&caller, &call_id, &request);
1080 tokio::pin!(execution);
1081 tokio::select! {
1082 _ = caller.cancellation.cancelled() => Err(AgentError::Cancelled),
1083 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(caller.deadline)) => {
1084 Err(AgentError::TimedOut)
1085 }
1086 result = &mut execution => result,
1087 }
1088 };
1089 let result = match execution {
1090 Ok(result) => result,
1091 Err(error) => failed_call_result(call_id.clone(), request.target.clone(), &error),
1092 };
1093 emit(
1094 &caller,
1095 AgentEvent::AgentCallCompleted {
1096 call_id,
1097 target: result.target.clone(),
1098 outcome: result.outcome.kind(),
1099 },
1100 );
1101 result
1102 }
1103
1104 async fn try_execute_agent_call(
1105 self: &Arc<Self>,
1106 caller: &RunContext,
1107 call_id: &AgentCallId,
1108 request: &AgentCallRequest,
1109 ) -> Result<AgentCallResult, AgentError> {
1110 if caller.cancellation.is_cancelled() {
1111 return Err(AgentError::Cancelled);
1112 }
1113 if Instant::now() >= caller.deadline {
1114 return Err(AgentError::TimedOut);
1115 }
1116 request.target.validate()?;
1117 if request.task.trim().is_empty() {
1118 return Err(AgentError::InvalidConfiguration(
1119 "Agent call task is required".to_string(),
1120 ));
1121 }
1122 if request.max_iterations == Some(0) {
1123 return Err(AgentError::InvalidConfiguration(
1124 "Agent call max_iterations must be greater than zero".to_string(),
1125 ));
1126 }
1127 if request.timeout.is_some_and(|timeout| timeout.is_zero()) {
1128 return Err(AgentError::InvalidConfiguration(
1129 "Agent call timeout must be greater than zero".to_string(),
1130 ));
1131 }
1132 let context_bytes = request
1133 .context
1134 .as_ref()
1135 .and_then(|value| serde_json::to_vec(value).ok())
1136 .map(|value| value.len())
1137 .unwrap_or(0);
1138 if context_bytes > caller.budget.limits().runtime.max_context_transfer_bytes {
1139 return Err(AgentError::BudgetExceeded(
1140 "max_context_transfer_bytes".to_string(),
1141 ));
1142 }
1143
1144 let target = caller.snapshot.agent(&request.target)?;
1145 if !caller.agent.callable_agents.allows(&target.id)
1146 || !caller.snapshot.policy.callable_agents.allows(&target.id)
1147 {
1148 return Err(AgentError::AgentCallDenied {
1149 caller: caller.agent.id.to_string(),
1150 target: target.id.to_string(),
1151 });
1152 }
1153
1154 if !caller.snapshot.policy.allow_recursive_calls
1155 && caller.ancestry.iter().any(|agent| agent == &target.id)
1156 {
1157 return Err(AgentError::AgentCallCycle(format!(
1158 "{} -> {}",
1159 caller
1160 .ancestry
1161 .iter()
1162 .map(ToString::to_string)
1163 .collect::<Vec<_>>()
1164 .join(" -> "),
1165 target.id
1166 )));
1167 }
1168
1169 let depth = caller.depth + 1;
1170 if depth > caller.budget.limits().runtime.max_depth {
1171 return Err(AgentError::MaxDepthExceeded(
1172 caller.budget.limits().runtime.max_depth,
1173 ));
1174 }
1175
1176 match self
1177 .authorizer
1178 .authorize(&caller.view(), &target, request)
1179 .await?
1180 {
1181 AuthorizationDecision::Allow => {}
1182 AuthorizationDecision::Deny(reason) => {
1183 return Err(AgentError::AgentCallDenied {
1184 caller: caller.agent.id.to_string(),
1185 target: format!("{} ({reason})", target.id),
1186 })
1187 }
1188 }
1189
1190 caller.reserve_child()?;
1191 caller.budget.reserve_agent_call()?;
1192 caller.budget.reserve_run()?;
1193
1194 let model = caller
1195 .snapshot
1196 .resolve_model(&target, Some(&caller.model.id))?;
1197 let child_run_id = RunId::new();
1198 let child_session_id = uuid::Uuid::new_v4().to_string();
1199 let mut child_session = caller.session.clone();
1200 child_session.session_id = Some(child_session_id.clone());
1201 let child_input = build_child_input(caller, &target, request).await?;
1202 let cancellation = caller.cancellation.child_token();
1203 let deadline = run_deadline(
1204 Instant::now(),
1205 caller.deadline.saturating_duration_since(Instant::now()),
1206 caller.budget.limits().runtime.run_timeout,
1207 target.limits.timeout_millis,
1208 request.timeout,
1209 )
1210 .min(caller.deadline);
1211 let mut ancestry = caller.ancestry.clone();
1212 ancestry.push(target.id.clone());
1213 let metadata = metadata_with_lineage(
1214 &request.metadata,
1215 &caller.root_run_id,
1216 Some(&caller.run_id),
1217 Some(call_id),
1218 &target.id,
1219 depth,
1220 );
1221 let child_context = RunContext {
1222 run_id: child_run_id.clone(),
1223 root_run_id: caller.root_run_id.clone(),
1224 parent_run_id: Some(caller.run_id.clone()),
1225 agent_call_id: Some(call_id.clone()),
1226 agent: Arc::clone(&target),
1227 model,
1228 session_id: child_session_id,
1229 parent_session_id: Some(caller.session_id.clone()),
1230 session: child_session,
1231 runtime_context: caller.runtime_context.clone(),
1232 depth,
1233 ancestry,
1234 deadline,
1235 max_iterations_override: request.max_iterations,
1236 result_contract: Some(request.result_contract.clone()),
1237 cancellation,
1238 budget: Arc::clone(&caller.budget),
1239 snapshot: Arc::clone(&caller.snapshot),
1240 events: caller.events.clone(),
1241 child_count: Arc::new(AtomicUsize::new(0)),
1242 active_children: Arc::new(AtomicUsize::new(0)),
1243 usage: Arc::new(std::sync::Mutex::new(RunUsage::default())),
1244 metadata,
1245 };
1246
1247 let (command_tx, command_rx) = mpsc::channel(COMMAND_CHANNEL_CAPACITY);
1248 let lease = self
1249 .supervisor
1250 .register(child_context.clone(), command_tx)
1251 .await?;
1252 if let Err(error) = self.create_run_record(&child_context).await {
1253 self.supervisor.unregister(&child_run_id).await;
1254 drop(lease);
1255 return Err(error);
1256 }
1257
1258 let active_children = caller.active_children.fetch_add(1, Ordering::SeqCst) + 1;
1259 if active_children == 1 {
1260 let _ = self
1261 .supervisor
1262 .run_store
1263 .update_status(&caller.run_id, RunStatus::WaitingForChild)
1264 .await;
1265 }
1266
1267 let runtime = Arc::clone(self);
1268 let child_task = tokio::spawn(async move {
1269 runtime
1270 .execute_run(
1271 child_context,
1272 AgentInput::Text(child_input),
1273 command_rx,
1274 lease,
1275 )
1276 .await
1277 });
1278 let result = child_task.await;
1279
1280 if caller.active_children.fetch_sub(1, Ordering::SeqCst) == 1 {
1281 let _ = self
1282 .supervisor
1283 .run_store
1284 .update_status(&caller.run_id, RunStatus::Running)
1285 .await;
1286 }
1287 let result = result.map_err(|error| {
1288 AgentError::Internal(format!(
1289 "child Agent run task stopped unexpectedly: {error}"
1290 ))
1291 })?;
1292
1293 Ok(call_result_from_run(
1294 call_id.clone(),
1295 target.id.clone(),
1296 result,
1297 ))
1298 }
1299}
1300
1301fn validate_root_request(request: &AgentRunRequest) -> Result<(), AgentError> {
1302 request.entry_agent.validate()?;
1303 match &request.input {
1304 AgentInput::Text(value) if value.trim().is_empty() => {
1305 return Err(AgentError::InvalidConfiguration(
1306 "Agent input is required".to_string(),
1307 ))
1308 }
1309 AgentInput::Messages(messages) if messages.is_empty() => {
1310 return Err(AgentError::InvalidConfiguration(
1311 "Agent input messages are required".to_string(),
1312 ))
1313 }
1314 AgentInput::Text(_) | AgentInput::Messages(_) => {}
1315 }
1316 if request.session.context_window == 0 {
1317 return Err(AgentError::InvalidConfiguration(
1318 "context_window must be greater than zero".to_string(),
1319 ));
1320 }
1321 if request
1322 .session
1323 .session_id
1324 .as_ref()
1325 .is_some_and(|session_id| session_id.trim().is_empty() || session_id.len() > 256)
1326 {
1327 return Err(AgentError::InvalidConfiguration(
1328 "session_id must contain 1-256 characters".to_string(),
1329 ));
1330 }
1331 if request
1332 .session
1333 .threshold_percentage
1334 .is_some_and(|value| !value.is_finite() || value <= 0.0 || value > 1.0)
1335 {
1336 return Err(AgentError::InvalidConfiguration(
1337 "threshold_percentage must be greater than 0 and at most 1".to_string(),
1338 ));
1339 }
1340 let uses_database = request.session.context_load_type == ContextLoadType::Database
1341 || request.session.storage_type == StorageType::Database;
1342 if uses_database {
1343 if request.session.database_config.is_none() {
1344 return Err(AgentError::InvalidConfiguration(
1345 "database_config is required for database storage or context".to_string(),
1346 ));
1347 }
1348 let runtime_context = request.runtime_context.as_ref().ok_or_else(|| {
1349 AgentError::InvalidConfiguration(
1350 "runtime_context is required for database storage or context".to_string(),
1351 )
1352 })?;
1353 runtime_context
1354 .validate()
1355 .map_err(AgentError::InvalidConfiguration)?;
1356 } else if let Some(runtime_context) = &request.runtime_context {
1357 runtime_context
1358 .validate()
1359 .map_err(AgentError::InvalidConfiguration)?;
1360 }
1361 if request
1362 .idempotency_key
1363 .as_ref()
1364 .is_some_and(|key| key.is_empty() || key.len() > 256)
1365 {
1366 return Err(AgentError::InvalidConfiguration(
1367 "idempotency_key must contain 1-256 characters".to_string(),
1368 ));
1369 }
1370 Ok(())
1371}
1372
1373fn run_deadline(
1374 now: Instant,
1375 root_timeout: Duration,
1376 run_timeout: Duration,
1377 agent_timeout_millis: Option<u64>,
1378 call_timeout: Option<Duration>,
1379) -> Instant {
1380 let mut timeout = root_timeout.min(run_timeout);
1381 if let Some(value) = agent_timeout_millis {
1382 timeout = timeout.min(Duration::from_millis(value));
1383 }
1384 if let Some(value) = call_timeout {
1385 timeout = timeout.min(value);
1386 }
1387 now + timeout
1388}
1389
1390fn effective_result_limit(context: &RunContext) -> usize {
1391 context
1392 .agent
1393 .limits
1394 .max_result_bytes
1395 .unwrap_or(context.budget.limits().runtime.max_result_bytes)
1396 .min(context.budget.limits().runtime.max_result_bytes)
1397}
1398
1399fn emit(context: &RunContext, event: AgentEvent) {
1400 context.events.emit(
1401 &context.root_run_id,
1402 &context.run_id,
1403 context.parent_run_id.as_ref(),
1404 &context.agent.id,
1405 context.depth,
1406 event,
1407 );
1408}
1409
1410fn error_outcome(error: &AgentError) -> (RunOutcome, RunStatus, AgentEvent, Option<AgentFailure>) {
1411 match error {
1412 AgentError::Cancelled => (
1413 RunOutcome::Cancelled,
1414 RunStatus::Cancelled,
1415 AgentEvent::RunCancelled,
1416 None,
1417 ),
1418 AgentError::TimedOut => (
1419 RunOutcome::TimedOut,
1420 RunStatus::TimedOut,
1421 AgentEvent::RunFailed {
1422 failure: failure_from_error(error),
1423 },
1424 Some(failure_from_error(error)),
1425 ),
1426 AgentError::BudgetExceeded(_) | AgentError::MaxIterationsExceeded(_) => (
1427 RunOutcome::BudgetExceeded,
1428 RunStatus::BudgetExceeded,
1429 AgentEvent::RunFailed {
1430 failure: failure_from_error(error),
1431 },
1432 Some(failure_from_error(error)),
1433 ),
1434 _ => {
1435 let failure = failure_from_error(error);
1436 (
1437 RunOutcome::Failed(failure.clone()),
1438 RunStatus::Failed,
1439 AgentEvent::RunFailed {
1440 failure: failure.clone(),
1441 },
1442 Some(failure),
1443 )
1444 }
1445 }
1446}
1447
1448fn failure_from_error(error: &AgentError) -> AgentFailure {
1449 let code = match error {
1450 AgentError::InvalidConfiguration(_) => "invalid_configuration",
1451 AgentError::AgentNotFound(_) => "agent_not_found",
1452 AgentError::AgentCallDenied { .. } => "agent_call_denied",
1453 AgentError::AgentCallCycle(_) => "agent_call_cycle",
1454 AgentError::MaxDepthExceeded(_) => "max_depth_exceeded",
1455 AgentError::BudgetExceeded(_) | AgentError::MaxIterationsExceeded(_) => "budget_exceeded",
1456 AgentError::SessionBusy(_) => "session_busy",
1457 AgentError::TimedOut => "timed_out",
1458 AgentError::Cancelled => "cancelled",
1459 AgentError::AiError(_) => "model_error",
1460 AgentError::ContextError(_) => "context_error",
1461 AgentError::ToolError(_) => "tool_error",
1462 AgentError::StorageError(_) => "storage_error",
1463 AgentError::Internal(_) => "internal_error",
1464 };
1465 AgentFailure {
1466 code: code.to_string(),
1467 message: error.to_string(),
1468 retryable: matches!(
1469 error,
1470 AgentError::TimedOut | AgentError::AiError(_) | AgentError::SessionBusy(_)
1471 ),
1472 }
1473}
1474
1475fn failed_call_result(
1476 call_id: AgentCallId,
1477 target: AgentId,
1478 error: &AgentError,
1479) -> AgentCallResult {
1480 AgentCallResult {
1481 schema_version: 1,
1482 call_id,
1483 target,
1484 run_id: None,
1485 outcome: RunOutcome::Failed(failure_from_error(error)),
1486 output: None,
1487 usage: RunUsage::default(),
1488 artifacts: Vec::new(),
1489 }
1490}
1491
1492fn call_result_from_run(
1493 call_id: AgentCallId,
1494 target: AgentId,
1495 result: AgentRunResult,
1496) -> AgentCallResult {
1497 AgentCallResult {
1498 schema_version: 1,
1499 call_id,
1500 target,
1501 run_id: Some(result.run_id),
1502 outcome: result.outcome,
1503 output: result.output,
1504 usage: result.usage,
1505 artifacts: result.artifacts,
1506 }
1507}
1508
1509fn apply_result_contract(
1510 output: Option<AgentOutput>,
1511 contract: &ResultContract,
1512) -> Result<Option<AgentOutput>, AgentFailure> {
1513 let Some(output) = output else {
1514 return Ok(None);
1515 };
1516 match contract {
1517 ResultContract::Text => match output {
1518 AgentOutput::Text(value) => Ok(Some(AgentOutput::Text(value))),
1519 AgentOutput::Json(value) => Ok(Some(AgentOutput::Text(value.to_string()))),
1520 },
1521 ResultContract::Auto => match output {
1522 AgentOutput::Text(value) => match serde_json::from_str(&value) {
1523 Ok(json) => Ok(Some(AgentOutput::Json(json))),
1524 Err(_) => Ok(Some(AgentOutput::Text(value))),
1525 },
1526 AgentOutput::Json(value) => Ok(Some(AgentOutput::Json(value))),
1527 },
1528 ResultContract::Json { schema } => {
1529 let value = match output {
1530 AgentOutput::Json(value) => value,
1531 AgentOutput::Text(value) => serde_json::from_str(&value).map_err(|error| {
1532 invalid_output_failure(format!("invalid JSON Agent output: {error}"))
1533 })?,
1534 };
1535 if let Some(schema) = schema {
1536 if !schema.is_object() {
1537 return Err(invalid_output_failure(
1538 "Agent result JSON schema must be an object",
1539 ));
1540 }
1541 validate_basic_json_schema(&value, schema).map_err(invalid_output_failure)?;
1542 }
1543 Ok(Some(AgentOutput::Json(value)))
1544 }
1545 }
1546}
1547
1548fn invalid_output_failure(message: impl Into<String>) -> AgentFailure {
1549 AgentFailure {
1550 code: "invalid_agent_output".to_string(),
1551 message: message.into(),
1552 retryable: true,
1553 }
1554}
1555
1556fn validate_basic_json_schema(
1557 value: &serde_json::Value,
1558 schema: &serde_json::Value,
1559) -> Result<(), String> {
1560 if let Some(expected_type) = schema.get("type").and_then(|value| value.as_str()) {
1561 let valid = match expected_type {
1562 "object" => value.is_object(),
1563 "array" => value.is_array(),
1564 "string" => value.is_string(),
1565 "number" => value.is_number(),
1566 "integer" => value.as_i64().is_some() || value.as_u64().is_some(),
1567 "boolean" => value.is_boolean(),
1568 "null" => value.is_null(),
1569 _ => true,
1570 };
1571 if !valid {
1572 return Err(format!("Agent output must be JSON type '{expected_type}'"));
1573 }
1574 }
1575 if let (Some(object), Some(required)) = (
1576 value.as_object(),
1577 schema.get("required").and_then(|value| value.as_array()),
1578 ) {
1579 for key in required.iter().filter_map(|value| value.as_str()) {
1580 if !object.contains_key(key) {
1581 return Err(format!("Agent output is missing required field '{key}'"));
1582 }
1583 }
1584 }
1585 Ok(())
1586}
1587
1588fn system_prompt_for(context: &RunContext) -> String {
1589 let skill_policy = context.snapshot.skill_policy_for(&context.agent);
1590 let skills = context.snapshot.skills.format_for_prompt(&skill_policy);
1591 let memory = match context.agent.memory {
1592 MemoryPolicy::Disabled => "",
1593 MemoryPolicy::ReadOnlyShared => {
1594 "\nLong-term memory is read-only. Use memory.recall only when relevant."
1595 }
1596 MemoryPolicy::ReadWriteShared | MemoryPolicy::PrivateAgent => {
1597 "\nLong-term memory is available. Recall relevant facts before answering and store only durable, non-sensitive information."
1598 }
1599 };
1600 format!("{}{}{}", context.agent.system_prompt, skills, memory)
1601}
1602
1603fn tool_registry_for(context: &RunContext) -> Result<(Arc<ToolRegistry>, Vec<Tool>), AgentError> {
1604 let names = context.snapshot.tool_names_for(&context.agent);
1605 let mut registry = context
1606 .snapshot
1607 .tools
1608 .subset(names.iter().map(String::as_str))
1609 .map_err(|error| AgentError::ToolError(error.to_string()))?;
1610
1611 let reachable = context.snapshot.reachable_agents(&context.agent);
1612 if !reachable.is_empty() {
1613 let mut registration = context
1614 .snapshot
1615 .tools
1616 .get(AGENT_CALL_TOOL_NAME)
1617 .cloned()
1618 .ok_or_else(|| AgentError::Internal("Agent call tool is not registered".to_string()))?;
1619 registration.definition = agent_call_tool_definition(&reachable);
1620 registry
1621 .register(registration)
1622 .map_err(|error| AgentError::ToolError(error.to_string()))?;
1623 }
1624
1625 let definitions = registry.definitions();
1626 Ok((Arc::new(registry), definitions))
1627}
1628
1629fn agent_call_tool_definition(reachable: &[Arc<AgentDefinition>]) -> Tool {
1630 let names = reachable
1631 .iter()
1632 .map(|agent| serde_json::Value::String(agent.id.to_string()))
1633 .collect::<Vec<_>>();
1634 let descriptions = reachable
1635 .iter()
1636 .map(|agent| format!("- {}: {}", agent.id, agent.description))
1637 .collect::<Vec<_>>()
1638 .join("\n");
1639 Tool {
1640 tool_type: "function".to_string(),
1641 function: FunctionDefinition {
1642 name: AGENT_CALL_TOOL_NAME.to_string(),
1643 description: format!(
1644 "Delegate one focused task to another Agent and wait for its structured result.\n{descriptions}"
1645 ),
1646 parameters: Some(serde_json::json!({
1647 "type": "object",
1648 "properties": {
1649 "agent": { "type": "string", "enum": names },
1650 "task": { "type": "string" },
1651 "context": { "type": "object" }
1652 },
1653 "required": ["agent", "task"],
1654 "additionalProperties": false
1655 })),
1656 },
1657 }
1658}
1659
1660fn build_model_config(
1661 context: &RunContext,
1662 messages: &[Message],
1663 tools: &[Tool],
1664) -> serde_json::Value {
1665 use otherone_ai::types::ProviderType;
1666
1667 let overrides = &context.agent.model_overrides;
1668 let mut config = serde_json::json!({
1669 "model": context.model.model,
1670 "messages": messages,
1671 "stream": true,
1672 });
1673 let provider = &context.model.provider;
1674 let context_length = overrides.context_length.or(context.model.context_length);
1675 let max_tokens = overrides.max_tokens.or(context.model.max_tokens);
1676 if let Some(value) = context_length.filter(|_| {
1677 matches!(
1678 provider,
1679 ProviderType::OpenAI | ProviderType::OpenRouter | ProviderType::Local
1680 )
1681 }) {
1682 config["contextLength"] = serde_json::json!(value);
1683 }
1684 let effective_max_tokens = if matches!(provider, ProviderType::Fetch) {
1685 max_tokens.or(context_length)
1686 } else {
1687 max_tokens
1688 };
1689 if let Some(value) = effective_max_tokens {
1690 let key = match provider {
1691 ProviderType::Anthropic | ProviderType::Fetch => "max_tokens",
1692 ProviderType::OpenAI | ProviderType::OpenRouter | ProviderType::Local => "maxTokens",
1693 };
1694 config[key] = serde_json::json!(value);
1695 }
1696 if let Some(value) = overrides.temperature.or(context.model.temperature) {
1697 config["temperature"] = serde_json::json!(value);
1698 }
1699 if let Some(value) = overrides.top_p.or(context.model.top_p) {
1700 let key = if matches!(provider, ProviderType::Fetch) {
1701 "top_p"
1702 } else {
1703 "topP"
1704 };
1705 config[key] = serde_json::json!(value);
1706 }
1707 if !tools.is_empty() {
1708 config["tools"] = serde_json::json!(tools);
1709 }
1710 if let Some(value) = &overrides.tool_choice {
1711 match provider {
1712 ProviderType::Fetch => config["tool_choice"] = serde_json::json!(value),
1713 ProviderType::OpenAI | ProviderType::OpenRouter | ProviderType::Local => {
1714 config["toolChoice"] = serde_json::json!(value)
1715 }
1716 ProviderType::Anthropic => {}
1717 }
1718 }
1719 if let Some(value) = overrides.parallel_tool_calls.filter(|_| {
1720 matches!(
1721 provider,
1722 ProviderType::OpenAI | ProviderType::OpenRouter | ProviderType::Local
1723 )
1724 }) {
1725 config["parallelToolCalls"] = serde_json::json!(value);
1726 }
1727 if let Some(serde_json::Value::Object(values)) =
1728 merge_other(context.model.other.as_ref(), overrides.other.as_ref())
1729 {
1730 for (key, value) in values {
1731 config[key] = value;
1732 }
1733 }
1734 config
1735}
1736
1737async fn combine_context(
1738 context: &RunContext,
1739 system_prompt: &str,
1740 tools: &[Tool],
1741) -> Result<Vec<Message>, AgentError> {
1742 let load_type = match context.session.context_load_type {
1743 ContextLoadType::LocalFile => otherone_context::types::ContextLoadType::LocalFile,
1744 ContextLoadType::Database => otherone_context::types::ContextLoadType::Database,
1745 };
1746 otherone_context::combine_context(&otherone_context::types::CombineContextOptions {
1747 session_id: context.session_id.clone(),
1748 load_type,
1749 provider: context.model.provider.clone(),
1750 context_window: context.session.context_window,
1751 threshold_percentage: context.session.threshold_percentage,
1752 ai: merge_other(
1753 context.model.other.as_ref(),
1754 context.agent.model_overrides.other.as_ref(),
1755 ),
1756 system_prompt: (!system_prompt.is_empty()).then(|| system_prompt.to_string()),
1757 tools: (!tools.is_empty()).then(|| tools.to_vec()),
1758 database_config: context.session.database_config.clone(),
1759 runtime_context: context.runtime_context.clone(),
1760 })
1761 .await
1762 .map_err(|error| AgentError::ContextError(error.to_string()))
1763}
1764
1765struct ModelTurn {
1766 content: String,
1767 role: String,
1768 tool_calls: Vec<ToolCall>,
1769 input_tokens: Option<u64>,
1770 output_tokens: Option<u64>,
1771 total_tokens: Option<u64>,
1772}
1773
1774impl ModelTurn {
1775 fn usage(&self) -> RunUsage {
1776 RunUsage {
1777 input_tokens: self.input_tokens,
1778 output_tokens: self.output_tokens,
1779 total_tokens: self.total_tokens,
1780 model_calls: 1,
1781 ..Default::default()
1782 }
1783 }
1784}
1785
1786async fn collect_model_stream(
1787 context: &RunContext,
1788 mut stream: ChatStream,
1789) -> Result<ModelTurn, AgentError> {
1790 let mut content = String::new();
1791 let mut role = "assistant".to_string();
1792 let mut tool_calls = Vec::new();
1793 let mut input_tokens = None;
1794 let mut output_tokens = None;
1795 let mut total_tokens = None;
1796 let output_limit = effective_result_limit(context);
1797 let mut tool_payload_bytes = 0usize;
1798
1799 while let Some(chunk) = stream.next().await {
1800 let chunk = chunk?;
1801 if let Some(choice) = chunk.choices.first() {
1802 if let Some(delta) = &choice.delta {
1803 if let Some(value) = &delta.role {
1804 role = value.clone();
1805 }
1806 if let Some(value) = &delta.content {
1807 if content
1808 .len()
1809 .saturating_add(tool_payload_bytes)
1810 .saturating_add(value.len())
1811 > output_limit
1812 {
1813 return Err(AgentError::BudgetExceeded("max_result_bytes".to_string()));
1814 }
1815 content.push_str(value);
1816 emit(
1817 context,
1818 AgentEvent::ModelDelta {
1819 content: value.clone(),
1820 },
1821 );
1822 }
1823 if let Some(value) = delta_thinking(delta) {
1824 emit(
1825 context,
1826 AgentEvent::ThinkingDelta {
1827 content: value.to_string(),
1828 },
1829 );
1830 }
1831 if let Some(calls) = &delta.tool_calls {
1832 for call in calls {
1833 tool_payload_bytes = tool_payload_bytes
1834 .saturating_add(call.id.len())
1835 .saturating_add(call.function.name.len())
1836 .saturating_add(call.function.arguments.len());
1837 if content.len().saturating_add(tool_payload_bytes) > output_limit {
1838 return Err(AgentError::BudgetExceeded("max_result_bytes".to_string()));
1839 }
1840 merge_tool_call_delta(&mut tool_calls, call);
1841 }
1842 }
1843 } else if let Some(message) = &choice.message {
1844 if let Some(value) = &message.role {
1845 role = value.clone();
1846 }
1847 if let Some(value) = &message.content {
1848 if content
1849 .len()
1850 .saturating_add(tool_payload_bytes)
1851 .saturating_add(value.len())
1852 > output_limit
1853 {
1854 return Err(AgentError::BudgetExceeded("max_result_bytes".to_string()));
1855 }
1856 content.push_str(value);
1857 emit(
1858 context,
1859 AgentEvent::ModelDelta {
1860 content: value.clone(),
1861 },
1862 );
1863 }
1864 if let Some(calls) = &message.tool_calls {
1865 tool_payload_bytes = serde_json::to_vec(calls)
1866 .map(|value| value.len())
1867 .unwrap_or(usize::MAX);
1868 if content.len().saturating_add(tool_payload_bytes) > output_limit {
1869 return Err(AgentError::BudgetExceeded("max_result_bytes".to_string()));
1870 }
1871 tool_calls = calls.clone();
1872 }
1873 }
1874 }
1875 if let Some(usage) = chunk.usage {
1876 if let Some(value) = usage.prompt_tokens {
1877 input_tokens = Some(u64::from(value));
1878 }
1879 if let Some(value) = usage.completion_tokens {
1880 output_tokens = Some(u64::from(value));
1881 }
1882 if let Some(value) = usage.total_tokens {
1883 total_tokens = Some(u64::from(value));
1884 }
1885 }
1886 }
1887
1888 if let (Some(input), Some(output)) = (input_tokens, output_tokens) {
1889 total_tokens = Some(input.saturating_add(output));
1890 }
1891
1892 Ok(ModelTurn {
1893 content,
1894 role,
1895 tool_calls,
1896 input_tokens,
1897 output_tokens,
1898 total_tokens,
1899 })
1900}
1901
1902fn delta_thinking(delta: &otherone_ai::types::ResponseDelta) -> Option<&str> {
1903 delta
1904 .reasoning_content
1905 .as_deref()
1906 .or(delta.reasoning.as_deref())
1907 .or(delta.thinking.as_deref())
1908 .or(delta.thought.as_deref())
1909 .filter(|value| !value.is_empty())
1910}
1911
1912fn merge_tool_call_delta(tool_calls: &mut Vec<ToolCall>, delta: &ToolCall) {
1913 let position = delta
1914 .index
1915 .and_then(|index| tool_calls.iter().position(|call| call.index == Some(index)))
1916 .or_else(|| {
1917 (!delta.id.is_empty())
1918 .then(|| tool_calls.iter().position(|call| call.id == delta.id))
1919 .flatten()
1920 });
1921
1922 if let Some(position) = position {
1923 let target = &mut tool_calls[position];
1924 if !delta.id.is_empty() {
1925 target.id = delta.id.clone();
1926 }
1927 if !delta.function.name.is_empty() {
1928 target.function.name = delta.function.name.clone();
1929 }
1930 target
1931 .function
1932 .arguments
1933 .push_str(&delta.function.arguments);
1934 if delta.index.is_some() {
1935 target.index = delta.index;
1936 }
1937 } else {
1938 tool_calls.push(delta.clone());
1939 }
1940}
1941
1942async fn persist_input(context: &RunContext, input: AgentInput) -> Result<(), AgentError> {
1943 match input {
1944 AgentInput::Text(value) => persist_entry(context, "user", &value, None, None).await,
1945 AgentInput::Messages(messages) => {
1946 for message in messages {
1947 let content = message_content_string(&message.content);
1948 let tools = message
1949 .tool_calls
1950 .as_ref()
1951 .map(|calls| serde_json::json!({ "tool_calls": calls }));
1952 persist_entry(context, &message.role, &content, tools, None).await?;
1953 }
1954 Ok(())
1955 }
1956 }
1957}
1958
1959fn message_content_string(content: &MessageContent) -> String {
1960 match content {
1961 MessageContent::Text(value) => value.clone(),
1962 MessageContent::MultiPart(value) => serde_json::to_string(value).unwrap_or_default(),
1963 }
1964}
1965
1966async fn persist_assistant_response(
1967 context: &RunContext,
1968 response: &ModelTurn,
1969) -> Result<(), AgentError> {
1970 let tools = (!response.tool_calls.is_empty())
1971 .then(|| serde_json::json!({ "tool_calls": response.tool_calls }));
1972 persist_entry(
1973 context,
1974 &response.role,
1975 &response.content,
1976 tools,
1977 response
1978 .total_tokens
1979 .map(|value| value.min(u32::MAX as u64) as u32),
1980 )
1981 .await
1982}
1983
1984async fn persist_tool_results(
1985 context: &RunContext,
1986 results: &[ToolResult],
1987) -> Result<(), AgentError> {
1988 for result in results {
1989 let content = match (&result.result, &result.error) {
1990 (Some(value), _) => serde_json::to_string(value).unwrap_or_else(|_| "null".to_string()),
1991 (None, Some(error)) => serde_json::json!({ "error": error }).to_string(),
1992 (None, None) => "null".to_string(),
1993 };
1994 let tools = serde_json::json!({
1995 "tool_call_id": result.tool_call_id,
1996 "function_name": result.function_name,
1997 "result": result.result,
1998 "error": result.error,
1999 "error_kind": result.error_kind,
2000 });
2001 persist_entry(context, "tool", &content, Some(tools), None).await?;
2002 }
2003 Ok(())
2004}
2005
2006async fn persist_entry(
2007 context: &RunContext,
2008 role: &str,
2009 content: &str,
2010 tools: Option<serde_json::Value>,
2011 token_consumption: Option<u32>,
2012) -> Result<(), AgentError> {
2013 let storage_type = match context.session.storage_type {
2014 StorageType::LocalFile => StorageBackend::LocalFile,
2015 StorageType::Database => StorageBackend::Database,
2016 };
2017 otherone_storage::write_entry(&WriteEntryOptions {
2018 storage_type,
2019 session_id: context.session_id.clone(),
2020 role: role.to_string(),
2021 content: content.to_string(),
2022 tools,
2023 token_consumption,
2024 create_at: None,
2025 database_config: context.session.database_config.clone(),
2026 runtime_context: context.runtime_context.clone(),
2027 metadata: context.metadata.clone(),
2028 })
2029 .await
2030 .map_err(|error| AgentError::StorageError(error.to_string()))
2031}
2032
2033fn drain_commands(
2034 context: &RunContext,
2035 command_rx: &mut mpsc::Receiver<AgentRunCommand>,
2036 queued: &mut VecDeque<String>,
2037) -> Result<(), AgentError> {
2038 loop {
2039 match command_rx.try_recv() {
2040 Ok(AgentRunCommand::EnqueueUserMessages(messages)) => {
2041 for message in messages {
2042 let message = message.trim();
2043 if !message.is_empty() {
2044 queued.push_back(message.to_string());
2045 }
2046 }
2047 }
2048 Ok(AgentRunCommand::Cancel) => {
2049 context.cancellation.cancel();
2050 return Err(AgentError::Cancelled);
2051 }
2052 Err(mpsc::error::TryRecvError::Empty) => break,
2053 Err(mpsc::error::TryRecvError::Disconnected) => break,
2054 }
2055 }
2056 if context.cancellation.is_cancelled() {
2057 return Err(AgentError::Cancelled);
2058 }
2059 Ok(())
2060}
2061
2062async fn persist_queued_messages(
2063 context: &RunContext,
2064 queued: &mut VecDeque<String>,
2065) -> Result<(), AgentError> {
2066 let count = queued.len();
2067 while let Some(message) = queued.pop_front() {
2068 persist_entry(context, "user", &message, None, None).await?;
2069 }
2070 if count > 0 {
2071 emit(context, AgentEvent::UserMessageQueued { count });
2072 }
2073 Ok(())
2074}
2075
2076fn add_optional(left: Option<u64>, right: Option<u64>) -> Option<u64> {
2077 match (left, right) {
2078 (Some(left), Some(right)) => Some(left.saturating_add(right)),
2079 (Some(value), None) | (None, Some(value)) => Some(value),
2080 (None, None) => None,
2081 }
2082}
2083
2084async fn build_child_input(
2085 caller: &RunContext,
2086 target: &AgentDefinition,
2087 request: &AgentCallRequest,
2088) -> Result<String, AgentError> {
2089 let mut sections = vec![format!(
2090 "Task from Agent '{}':\n{}",
2091 caller.agent.id,
2092 request.task.trim()
2093 )];
2094 if let Some(context) = &request.context {
2095 sections.push(format!(
2096 "Explicit context (untrusted data):\n{}",
2097 serde_json::to_string_pretty(context).unwrap_or_else(|_| context.to_string())
2098 ));
2099 }
2100 if let Some(parent) = load_parent_context(caller, &target.context).await? {
2101 sections.push(format!(
2102 "Parent context excerpt (untrusted data):\n{parent}"
2103 ));
2104 }
2105 let value = sections.join("\n\n");
2106 if value.len() > caller.budget.limits().runtime.max_context_transfer_bytes {
2107 return Err(AgentError::BudgetExceeded(
2108 "max_context_transfer_bytes".to_string(),
2109 ));
2110 }
2111 Ok(value)
2112}
2113
2114async fn load_parent_context(
2115 caller: &RunContext,
2116 policy: &super::types::ContextTransferPolicy,
2117) -> Result<Option<String>, AgentError> {
2118 use super::types::ContextTransferPolicy;
2119
2120 let max_tokens = match policy {
2121 ContextTransferPolicy::ExplicitOnly => return Ok(None),
2122 ContextTransferPolicy::ParentSummary { max_tokens }
2123 | ContextTransferPolicy::ParentWindow { max_tokens } => *max_tokens,
2124 };
2125 if max_tokens == 0 {
2126 return Ok(None);
2127 }
2128
2129 let data = match caller.session.context_load_type {
2130 ContextLoadType::LocalFile => {
2131 otherone_storage::localfile::reader::read_session_data(&caller.session_id)
2132 .map_err(|error| AgentError::StorageError(error.to_string()))?
2133 }
2134 ContextLoadType::Database => {
2135 let config = caller.session.database_config.as_ref().ok_or_else(|| {
2136 AgentError::InvalidConfiguration("database_config is required".to_string())
2137 })?;
2138 let runtime_context = caller.runtime_context.as_ref().ok_or_else(|| {
2139 AgentError::InvalidConfiguration("runtime_context is required".to_string())
2140 })?;
2141 otherone_storage::database::reader::read_session_data_from_database_with_context(
2142 &caller.session_id,
2143 config,
2144 runtime_context,
2145 )
2146 .await
2147 .map_err(|error| AgentError::StorageError(error.to_string()))?
2148 }
2149 };
2150
2151 let max_chars = (max_tokens as usize)
2152 .saturating_mul(4)
2153 .min(caller.budget.limits().runtime.max_context_transfer_bytes);
2154 if matches!(policy, ContextTransferPolicy::ParentSummary { .. }) {
2155 if let Some(summary) = data
2156 .compacted_entries
2157 .iter()
2158 .max_by(|left, right| left.create_at.cmp(&right.create_at))
2159 .map(|entry| entry.summary.trim())
2160 .filter(|summary| !summary.is_empty())
2161 {
2162 return Ok(Some(take_last_chars(summary, max_chars)));
2163 }
2164 }
2165
2166 let mut selected = Vec::new();
2167 let mut selected_bytes = 0usize;
2168 for entry in data.entries.iter().rev().filter(|entry| {
2169 (entry.role == "user" || entry.role == "assistant") && entry.tools.is_none()
2170 }) {
2171 let line = format!(
2172 "{}: {}",
2173 entry.role,
2174 take_last_chars(&entry.content, max_chars)
2175 );
2176 selected_bytes = selected_bytes.saturating_add(line.len());
2177 selected.push(line);
2178 if selected_bytes >= max_chars {
2179 break;
2180 }
2181 }
2182 selected.reverse();
2183 let safe_messages = selected.join("\n");
2184 if safe_messages.is_empty() {
2185 Ok(None)
2186 } else {
2187 Ok(Some(take_last_chars(&safe_messages, max_chars)))
2188 }
2189}
2190
2191fn take_last_chars(value: &str, max_chars: usize) -> String {
2192 if max_chars == 0 {
2193 return String::new();
2194 }
2195 let start = value
2196 .char_indices()
2197 .rev()
2198 .nth(max_chars.saturating_sub(1))
2199 .map(|(index, _)| index)
2200 .unwrap_or(0);
2201 value[start..].to_string()
2202}
2203
2204#[derive(Debug, Deserialize)]
2205struct AgentCallArguments {
2206 agent: String,
2207 task: String,
2208 context: Option<serde_json::Value>,
2209}
2210
2211struct AgentCallToolHandler {
2212 runtime: Weak<RuntimeInner>,
2213}
2214
2215#[async_trait]
2216impl ToolHandler for AgentCallToolHandler {
2217 async fn call(
2218 &self,
2219 arguments: serde_json::Value,
2220 context: ToolCallContext,
2221 ) -> Result<serde_json::Value, ToolError> {
2222 let arguments: AgentCallArguments = serde_json::from_value(arguments)
2223 .map_err(|error| ToolError::invalid_arguments(error.to_string()))?;
2224 let runtime = self.runtime.upgrade().ok_or_else(|| {
2225 ToolError::new(ToolErrorKind::Fatal, "Agent runtime has been dropped")
2226 })?;
2227 let caller_run_id = RunId::from_string(context.run_id)
2228 .map_err(|error| ToolError::invalid_arguments(error.to_string()))?;
2229 let call_id = if context.tool_call_id.is_empty() {
2230 AgentCallId::generated()
2231 } else {
2232 AgentCallId::new(context.tool_call_id)
2233 .map_err(|error| ToolError::invalid_arguments(error.to_string()))?
2234 };
2235 let request = AgentCallRequest {
2236 target: AgentId::unchecked(arguments.agent),
2237 task: arguments.task,
2238 context: arguments.context,
2239 result_contract: ResultContract::Auto,
2240 max_iterations: None,
2241 timeout: None,
2242 metadata: AttributeBag::new(),
2243 };
2244
2245 let result = RuntimeInner::call_agent(runtime, &caller_run_id, call_id, request)
2246 .await
2247 .map_err(|error| ToolError::recoverable(error.to_string()))?;
2248 serde_json::to_value(result)
2249 .map_err(|error| ToolError::new(ToolErrorKind::Fatal, error.to_string()))
2250 }
2251}
2252
2253fn agent_call_registration(runtime: Weak<RuntimeInner>) -> ToolRegistration {
2254 ToolRegistration::new(
2255 Tool {
2256 tool_type: "function".to_string(),
2257 function: FunctionDefinition {
2258 name: AGENT_CALL_TOOL_NAME.to_string(),
2259 description: "Call another Agent".to_string(),
2260 parameters: Some(serde_json::json!({
2261 "type": "object",
2262 "properties": {
2263 "agent": { "type": "string" },
2264 "task": { "type": "string" },
2265 "context": { "type": "object" }
2266 },
2267 "required": ["agent", "task"]
2268 })),
2269 },
2270 },
2271 Arc::new(AgentCallToolHandler { runtime }),
2272 )
2273 .execution_class(ToolExecutionClass::Delegating)
2274}
2275
2276#[derive(Debug, Deserialize)]
2277struct MemoryRecallArguments {
2278 #[serde(default)]
2279 memory_types: Vec<String>,
2280}
2281
2282struct MemoryRecallHandler {
2283 lock: Arc<Mutex<()>>,
2284}
2285
2286#[async_trait]
2287impl ToolHandler for MemoryRecallHandler {
2288 async fn call(
2289 &self,
2290 arguments: serde_json::Value,
2291 context: ToolCallContext,
2292 ) -> Result<serde_json::Value, ToolError> {
2293 let arguments: MemoryRecallArguments = serde_json::from_value(arguments)
2294 .map_err(|error| ToolError::invalid_arguments(error.to_string()))?;
2295 if arguments.memory_types.is_empty() {
2296 return Err(ToolError::invalid_arguments("memory_types is required"));
2297 }
2298 let _guard = self.lock.lock().await;
2299 let tree = otherone_memory::read_memory_tree()
2300 .map_err(|error| ToolError::new(ToolErrorKind::Fatal, error.to_string()))?;
2301 let scope = memory_scope(&context);
2302 let queries = arguments
2303 .memory_types
2304 .iter()
2305 .map(|value| value.trim().to_lowercase())
2306 .filter(|value| !value.is_empty())
2307 .collect::<Vec<_>>();
2308 let memories = tree
2309 .iter_points()
2310 .filter(|point| point.is_active())
2311 .filter_map(|point| {
2312 let types = point.types.as_deref()?;
2313 let unscoped = types.strip_prefix(&scope)?;
2314 queries
2315 .iter()
2316 .any(|query| {
2317 let value = unscoped.to_lowercase();
2318 value.contains(query) || query.contains(&value)
2319 })
2320 .then(|| point.storage.clone())
2321 .flatten()
2322 })
2323 .take(64)
2324 .collect::<Vec<_>>();
2325 Ok(serde_json::json!({ "memories": memories }))
2326 }
2327}
2328
2329#[derive(Debug, Deserialize)]
2330struct MemoryStoreArguments {
2331 storage: String,
2332 types: String,
2333}
2334
2335struct MemoryStoreHandler {
2336 lock: Arc<Mutex<()>>,
2337}
2338
2339#[async_trait]
2340impl ToolHandler for MemoryStoreHandler {
2341 async fn call(
2342 &self,
2343 arguments: serde_json::Value,
2344 context: ToolCallContext,
2345 ) -> Result<serde_json::Value, ToolError> {
2346 let arguments: MemoryStoreArguments = serde_json::from_value(arguments)
2347 .map_err(|error| ToolError::invalid_arguments(error.to_string()))?;
2348 let storage = arguments.storage.trim();
2349 let types = arguments.types.trim();
2350 if storage.is_empty() || types.is_empty() {
2351 return Err(ToolError::invalid_arguments(
2352 "storage and types are required",
2353 ));
2354 }
2355 if contains_sensitive_memory(storage) {
2356 return Err(ToolError::new(
2357 ToolErrorKind::PermissionDenied,
2358 "sensitive values must not be stored in long-term memory",
2359 ));
2360 }
2361 let _guard = self.lock.lock().await;
2362 let mut tree = otherone_memory::read_memory_tree()
2363 .map_err(|error| ToolError::new(ToolErrorKind::Fatal, error.to_string()))?;
2364 let mut point = otherone_memory::MemoryPoint::new_root(
2365 storage,
2366 format!("{}{}", memory_scope(&context), types),
2367 )
2368 .map_err(|error| ToolError::recoverable(error.to_string()))?;
2369 point
2370 .set_attribute("source_run_id", serde_json::json!(context.run_id))
2371 .map_err(|error| ToolError::recoverable(error.to_string()))?;
2372 point
2373 .set_attribute("source_agent_id", serde_json::json!(context.agent_id))
2374 .map_err(|error| ToolError::recoverable(error.to_string()))?;
2375 if let Some(session_id) = &context.session_id {
2376 point
2377 .set_attribute("source_session_id", serde_json::json!(session_id))
2378 .map_err(|error| ToolError::recoverable(error.to_string()))?;
2379 }
2380 let point_id = tree
2381 .insert_point(point)
2382 .map_err(|error| ToolError::recoverable(error.to_string()))?;
2383 otherone_memory::write_memory_tree(&tree)
2384 .map_err(|error| ToolError::new(ToolErrorKind::Fatal, error.to_string()))?;
2385 Ok(serde_json::json!({ "stored": true, "point_id": point_id }))
2386 }
2387}
2388
2389fn memory_scope(context: &ToolCallContext) -> String {
2390 let partition = context
2391 .runtime_context
2392 .as_ref()
2393 .and_then(|value| value.get("partition_key"))
2394 .and_then(|value| value.as_str())
2395 .unwrap_or("default");
2396 let private = context
2397 .metadata
2398 .get("memory_policy")
2399 .and_then(|value| value.as_str())
2400 == Some("private_agent");
2401 if private {
2402 format!("[scope:{partition}:agent:{}] ", context.agent_id)
2403 } else {
2404 format!("[scope:{partition}] ")
2405 }
2406}
2407
2408fn contains_sensitive_memory(value: &str) -> bool {
2409 let value = value.to_lowercase();
2410 [
2411 "api key",
2412 "api_key",
2413 "password",
2414 "密码",
2415 "验证码",
2416 "verification code",
2417 "credit card",
2418 "银行卡",
2419 ]
2420 .iter()
2421 .any(|pattern| value.contains(pattern))
2422}
2423
2424fn memory_recall_registration(lock: Arc<Mutex<()>>) -> ToolRegistration {
2425 ToolRegistration::new(
2426 Tool {
2427 tool_type: "function".to_string(),
2428 function: FunctionDefinition {
2429 name: MEMORY_RECALL_TOOL_NAME.to_string(),
2430 description: "Recall scoped long-term memories by semantic type.".to_string(),
2431 parameters: Some(serde_json::json!({
2432 "type": "object",
2433 "properties": {
2434 "memory_types": {
2435 "type": "array",
2436 "items": { "type": "string" },
2437 "minItems": 1,
2438 "maxItems": 8
2439 }
2440 },
2441 "required": ["memory_types"],
2442 "additionalProperties": false
2443 })),
2444 },
2445 },
2446 Arc::new(MemoryRecallHandler { lock }),
2447 )
2448}
2449
2450fn memory_store_registration(lock: Arc<Mutex<()>>) -> ToolRegistration {
2451 ToolRegistration::new(
2452 Tool {
2453 tool_type: "function".to_string(),
2454 function: FunctionDefinition {
2455 name: MEMORY_STORE_TOOL_NAME.to_string(),
2456 description: "Store one durable, non-sensitive long-term memory.".to_string(),
2457 parameters: Some(serde_json::json!({
2458 "type": "object",
2459 "properties": {
2460 "storage": { "type": "string" },
2461 "types": { "type": "string" }
2462 },
2463 "required": ["storage", "types"],
2464 "additionalProperties": false
2465 })),
2466 },
2467 },
2468 Arc::new(MemoryStoreHandler { lock }),
2469 )
2470}
2471
2472fn global_memory_lock() -> Arc<Mutex<()>> {
2473 static LOCK: std::sync::OnceLock<Arc<Mutex<()>>> = std::sync::OnceLock::new();
2474 Arc::clone(LOCK.get_or_init(|| Arc::new(Mutex::new(()))))
2475}
2476
2477#[cfg(test)]
2478mod tests {
2479 use std::pin::Pin;
2480 use std::sync::atomic::AtomicUsize as TestAtomicUsize;
2481 use std::sync::OnceLock;
2482
2483 use futures::stream;
2484 use otherone_ai::types::{ChatResponse, Choice, ResponseDelta, Usage};
2485
2486 use super::*;
2487 use crate::multi_agent::RunLimitOverrides;
2488
2489 struct MockModelExecutor {
2490 responses: Mutex<VecDeque<ChatResponse>>,
2491 configs: Mutex<Vec<serde_json::Value>>,
2492 }
2493
2494 impl MockModelExecutor {
2495 fn new(responses: Vec<ChatResponse>) -> Self {
2496 Self {
2497 responses: Mutex::new(responses.into()),
2498 configs: Mutex::new(Vec::new()),
2499 }
2500 }
2501
2502 async fn configs(&self) -> Vec<serde_json::Value> {
2503 self.configs.lock().await.clone()
2504 }
2505 }
2506
2507 #[async_trait]
2508 impl ModelExecutor for MockModelExecutor {
2509 async fn stream(
2510 &self,
2511 _profile: &ModelProfile,
2512 config: serde_json::Value,
2513 ) -> Result<ChatStream, AiError> {
2514 self.configs.lock().await.push(config);
2515 let response = self
2516 .responses
2517 .lock()
2518 .await
2519 .pop_front()
2520 .ok_or_else(|| AiError::Other("no mock response".to_string()))?;
2521 Ok(Box::pin(stream::iter(vec![Ok(response)])) as Pin<Box<_>>)
2522 }
2523 }
2524
2525 fn response(content: &str, tool_calls: Option<Vec<ToolCall>>) -> ChatResponse {
2526 ChatResponse {
2527 id: Some(uuid::Uuid::new_v4().to_string()),
2528 object: None,
2529 created: None,
2530 model: Some("mock".to_string()),
2531 choices: vec![Choice {
2532 index: 0,
2533 message: None,
2534 delta: Some(ResponseDelta {
2535 role: Some("assistant".to_string()),
2536 content: Some(content.to_string()),
2537 reasoning_content: None,
2538 reasoning: None,
2539 thinking: None,
2540 thought: None,
2541 tool_calls,
2542 }),
2543 finish_reason: Some("stop".to_string()),
2544 }],
2545 usage: Some(Usage {
2546 prompt_tokens: Some(2),
2547 completion_tokens: Some(1),
2548 total_tokens: Some(3),
2549 }),
2550 }
2551 }
2552
2553 fn profile() -> ModelProfile {
2554 ModelProfile::builder("default", otherone_ai::types::ProviderType::OpenAI, "mock")
2555 .api_key("test")
2556 .base_url("http://localhost")
2557 .build()
2558 .unwrap()
2559 }
2560
2561 fn temp_storage() -> std::path::PathBuf {
2562 std::env::temp_dir().join(format!("otherone-multi-agent-{}", uuid::Uuid::new_v4()))
2563 }
2564
2565 #[test]
2566 fn memory_policy_controls_builtin_memory_tools() {
2567 let runtime = AgentRuntime::builder()
2568 .register_model(profile())
2569 .register_agent(
2570 AgentDefinition::builder("reader")
2571 .description("reader")
2572 .memory(MemoryPolicy::ReadOnlyShared)
2573 .build()
2574 .unwrap(),
2575 )
2576 .register_agent(
2577 AgentDefinition::builder("writer")
2578 .description("writer")
2579 .memory(MemoryPolicy::ReadWriteShared)
2580 .build()
2581 .unwrap(),
2582 )
2583 .build()
2584 .unwrap();
2585 let snapshot = runtime.snapshot();
2586 let reader = snapshot.agent(&AgentId::new("reader").unwrap()).unwrap();
2587 let writer = snapshot.agent(&AgentId::new("writer").unwrap()).unwrap();
2588 let reader_tools = snapshot.tool_names_for(&reader);
2589 let writer_tools = snapshot.tool_names_for(&writer);
2590 assert!(reader_tools.contains(MEMORY_RECALL_TOOL_NAME));
2591 assert!(!reader_tools.contains(MEMORY_STORE_TOOL_NAME));
2592 assert!(writer_tools.contains(MEMORY_RECALL_TOOL_NAME));
2593 assert!(writer_tools.contains(MEMORY_STORE_TOOL_NAME));
2594 }
2595
2596 #[tokio::test]
2597 async fn memory_writes_include_run_session_and_agent_provenance() {
2598 let _guard = storage_guard().await;
2599 otherone_memory::set_memory_storage_root(temp_storage());
2600 let handler = MemoryStoreHandler {
2601 lock: global_memory_lock(),
2602 };
2603 let mut context =
2604 ToolCallContext::new("run-1", "root-1", "agent-1", CancellationToken::new());
2605 context.session_id = Some("session-1".to_string());
2606 context.metadata = serde_json::json!({ "memory_policy": "read_write_shared" });
2607
2608 let result = handler
2609 .call(
2610 serde_json::json!({
2611 "storage": "The user prefers concise answers",
2612 "types": "response preference"
2613 }),
2614 context,
2615 )
2616 .await
2617 .unwrap();
2618 let point_id = result["point_id"].as_str().unwrap();
2619 let tree = otherone_memory::read_memory_tree().unwrap();
2620 let point = tree.get(point_id).unwrap();
2621 assert_eq!(point.attributes["source_run_id"], "run-1");
2622 assert_eq!(point.attributes["source_session_id"], "session-1");
2623 assert_eq!(point.attributes["source_agent_id"], "agent-1");
2624 otherone_memory::clear_memory_storage_root();
2625 }
2626
2627 #[test]
2628 fn agent_skill_policy_filters_the_runtime_skill_snapshot() {
2629 let root = std::env::temp_dir().join(format!("otherone-skill-{}", uuid::Uuid::new_v4()));
2630 std::fs::create_dir_all(&root).unwrap();
2631 let skill_path = root.join("SKILL.md");
2632 std::fs::write(
2633 &skill_path,
2634 "---\nname: test-skill\ndescription: Test skill description\n---\n\n# Test\n",
2635 )
2636 .unwrap();
2637 let registry =
2638 otherone_skills::SkillRegistry::load_from_config(&otherone_skills::SkillsConfig {
2639 include_defaults: false,
2640 user_skills_dir: None,
2641 project_skills_dir: None,
2642 extra_paths: vec![skill_path],
2643 cwd: root,
2644 })
2645 .unwrap();
2646 let runtime = AgentRuntime::builder()
2647 .register_model(profile())
2648 .register_agent(
2649 AgentDefinition::builder("agent")
2650 .description("agent")
2651 .allow_skills(["test-skill"])
2652 .build()
2653 .unwrap(),
2654 )
2655 .skills(®istry)
2656 .build()
2657 .unwrap();
2658 let snapshot = runtime.snapshot();
2659 let agent = snapshot.agent(&AgentId::new("agent").unwrap()).unwrap();
2660 let prompt = snapshot
2661 .skills
2662 .format_for_prompt(&snapshot.skill_policy_for(&agent));
2663 assert!(prompt.contains("test-skill"));
2664 assert!(prompt.contains("Test skill description"));
2665 }
2666
2667 async fn storage_guard() -> tokio::sync::MutexGuard<'static, ()> {
2668 static LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
2669 LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
2670 .lock()
2671 .await
2672 }
2673
2674 fn agent_call(id: &str, target: &str, task: &str) -> ToolCall {
2675 ToolCall {
2676 index: Some(0),
2677 id: id.to_string(),
2678 call_type: "function".to_string(),
2679 function: otherone_ai::types::FunctionCall {
2680 name: AGENT_CALL_TOOL_NAME.to_string(),
2681 arguments: serde_json::json!({ "agent": target, "task": task }).to_string(),
2682 },
2683 }
2684 }
2685
2686 #[tokio::test]
2687 async fn runs_a_root_agent_with_the_unified_runner() {
2688 let _guard = storage_guard().await;
2689 let root = temp_storage();
2690 otherone_storage::localfile::set_storage_root(root);
2691 let runtime = AgentRuntime::builder()
2692 .register_model(profile())
2693 .register_agent(
2694 AgentDefinition::builder("agent")
2695 .description("test agent")
2696 .build()
2697 .unwrap(),
2698 )
2699 .model_executor(Arc::new(MockModelExecutor::new(vec![response(
2700 "done", None,
2701 )])))
2702 .build()
2703 .unwrap();
2704
2705 let result = runtime
2706 .run(AgentRunRequest::new("agent", "hello"))
2707 .await
2708 .unwrap();
2709 assert_eq!(result.outcome, RunOutcome::Completed);
2710 assert_eq!(result.output, Some(AgentOutput::Text("done".to_string())));
2711 otherone_storage::localfile::clear_storage_root();
2712 }
2713
2714 #[tokio::test]
2715 async fn model_config_uses_provider_specific_parameter_names() {
2716 let _guard = storage_guard().await;
2717 otherone_storage::localfile::set_storage_root(temp_storage());
2718
2719 let anthropic_executor = Arc::new(MockModelExecutor::new(vec![response("done", None)]));
2720 let anthropic = AgentRuntime::builder()
2721 .register_model(
2722 ModelProfile::builder(
2723 "default",
2724 otherone_ai::types::ProviderType::Anthropic,
2725 "mock",
2726 )
2727 .api_key("test")
2728 .base_url("http://localhost")
2729 .max_tokens(321)
2730 .top_p(0.4)
2731 .build()
2732 .unwrap(),
2733 )
2734 .register_agent(
2735 AgentDefinition::builder("agent")
2736 .description("agent")
2737 .build()
2738 .unwrap(),
2739 )
2740 .model_executor(anthropic_executor.clone())
2741 .build()
2742 .unwrap();
2743 anthropic
2744 .run(AgentRunRequest::new("agent", "start"))
2745 .await
2746 .unwrap();
2747 let anthropic_config = anthropic_executor.configs().await.remove(0);
2748 assert_eq!(anthropic_config["max_tokens"], 321);
2749 assert!((anthropic_config["topP"].as_f64().unwrap() - 0.4).abs() < f64::from(f32::EPSILON));
2750 assert!(anthropic_config.get("maxTokens").is_none());
2751
2752 let fetch_executor = Arc::new(MockModelExecutor::new(vec![response("done", None)]));
2753 let fetch = AgentRuntime::builder()
2754 .register_model(
2755 ModelProfile::builder("default", otherone_ai::types::ProviderType::Fetch, "mock")
2756 .api_key("test")
2757 .base_url("http://localhost")
2758 .context_length(2048)
2759 .top_p(0.6)
2760 .build()
2761 .unwrap(),
2762 )
2763 .register_agent(
2764 AgentDefinition::builder("agent")
2765 .description("agent")
2766 .build()
2767 .unwrap(),
2768 )
2769 .model_executor(fetch_executor.clone())
2770 .build()
2771 .unwrap();
2772 fetch
2773 .run(AgentRunRequest::new("agent", "start"))
2774 .await
2775 .unwrap();
2776 let fetch_config = fetch_executor.configs().await.remove(0);
2777 assert_eq!(fetch_config["max_tokens"], 2048);
2778 assert!((fetch_config["top_p"].as_f64().unwrap() - 0.6).abs() < f64::from(f32::EPSILON));
2779 assert!(fetch_config.get("topP").is_none());
2780
2781 otherone_storage::localfile::clear_storage_root();
2782 }
2783
2784 #[tokio::test]
2785 async fn streaming_output_limit_stops_before_persisting_the_oversized_message() {
2786 let _guard = storage_guard().await;
2787 otherone_storage::localfile::set_storage_root(temp_storage());
2788 let runtime = AgentRuntime::builder()
2789 .register_model(profile())
2790 .register_agent(
2791 AgentDefinition::builder("agent")
2792 .description("agent")
2793 .build()
2794 .unwrap(),
2795 )
2796 .runtime_limits(RuntimeLimits {
2797 max_result_bytes: 4,
2798 ..Default::default()
2799 })
2800 .model_executor(Arc::new(MockModelExecutor::new(vec![response(
2801 "oversized",
2802 None,
2803 )])))
2804 .build()
2805 .unwrap();
2806
2807 let result = runtime
2808 .run(AgentRunRequest::new("agent", "start"))
2809 .await
2810 .unwrap();
2811 assert_eq!(result.outcome, RunOutcome::BudgetExceeded);
2812 let session =
2813 otherone_storage::localfile::reader::read_session_data(&result.session_id).unwrap();
2814 assert_eq!(session.entries.len(), 1);
2815 assert_eq!(session.entries[0].role, "user");
2816 otherone_storage::localfile::clear_storage_root();
2817 }
2818
2819 #[tokio::test]
2820 async fn agent_call_executes_child_and_returns_to_parent() {
2821 let _guard = storage_guard().await;
2822 let root = temp_storage();
2823 otherone_storage::localfile::set_storage_root(root);
2824 let call = agent_call("call_1", "worker", "do work");
2825 let runtime = AgentRuntime::builder()
2826 .register_model(profile())
2827 .register_agent(
2828 AgentDefinition::builder("coordinator")
2829 .description("coordinates")
2830 .allow_agents(["worker"])
2831 .build()
2832 .unwrap(),
2833 )
2834 .register_agent(
2835 AgentDefinition::builder("worker")
2836 .description("works")
2837 .build()
2838 .unwrap(),
2839 )
2840 .model_executor(Arc::new(MockModelExecutor::new(vec![
2841 response("", Some(vec![call])),
2842 response("worker result", None),
2843 response("final answer", None),
2844 ])))
2845 .build()
2846 .unwrap();
2847
2848 let result = runtime
2849 .run(AgentRunRequest::new("coordinator", "start"))
2850 .await
2851 .unwrap();
2852 assert_eq!(result.outcome, RunOutcome::Completed);
2853 assert_eq!(
2854 result.output,
2855 Some(AgentOutput::Text("final answer".to_string()))
2856 );
2857 let records = runtime.list_run_tree(&result.root_run_id).await;
2858 assert_eq!(records.len(), 2);
2859 assert!(records
2860 .iter()
2861 .any(|record| record.agent_id.as_str() == "worker"));
2862 assert_eq!(
2863 otherone_storage::localfile::reader::get_all_sessions()
2864 .unwrap()
2865 .len(),
2866 1
2867 );
2868 assert_eq!(
2869 otherone_storage::localfile::reader::get_all_sessions_with_internal(true)
2870 .unwrap()
2871 .len(),
2872 2
2873 );
2874 otherone_storage::localfile::clear_storage_root();
2875 }
2876
2877 #[tokio::test]
2878 async fn child_context_is_isolated_from_parent_history() {
2879 let _guard = storage_guard().await;
2880 otherone_storage::localfile::set_storage_root(temp_storage());
2881 let executor = Arc::new(MockModelExecutor::new(vec![
2882 response("", Some(vec![agent_call("call_1", "worker", "child task")])),
2883 response("child done", None),
2884 response("root done", None),
2885 ]));
2886 let runtime = AgentRuntime::builder()
2887 .register_model(profile())
2888 .register_agent(
2889 AgentDefinition::builder("root")
2890 .description("root")
2891 .allow_agents(["worker"])
2892 .build()
2893 .unwrap(),
2894 )
2895 .register_agent(
2896 AgentDefinition::builder("worker")
2897 .description("worker")
2898 .build()
2899 .unwrap(),
2900 )
2901 .model_executor(executor.clone())
2902 .build()
2903 .unwrap();
2904
2905 runtime
2906 .run(AgentRunRequest::new("root", "parent private history"))
2907 .await
2908 .unwrap();
2909 let configs = executor.configs().await;
2910 let child_messages = configs[1]["messages"].to_string();
2911 assert!(child_messages.contains("child task"));
2912 assert!(!child_messages.contains("parent private history"));
2913 otherone_storage::localfile::clear_storage_root();
2914 }
2915
2916 #[tokio::test]
2917 async fn model_cannot_execute_a_registered_but_disallowed_tool() {
2918 let _guard = storage_guard().await;
2919 otherone_storage::localfile::set_storage_root(temp_storage());
2920 let executions = Arc::new(TestAtomicUsize::new(0));
2921 let counter = Arc::clone(&executions);
2922 let dangerous_tool = ToolRegistration::sync(
2923 Tool {
2924 tool_type: "function".to_string(),
2925 function: FunctionDefinition {
2926 name: "dangerous".to_string(),
2927 description: "dangerous".to_string(),
2928 parameters: None,
2929 },
2930 },
2931 move |_| {
2932 counter.fetch_add(1, Ordering::SeqCst);
2933 "executed".to_string()
2934 },
2935 );
2936 let hallucinated_call = ToolCall {
2937 index: Some(0),
2938 id: "call_dangerous".to_string(),
2939 call_type: "function".to_string(),
2940 function: otherone_ai::types::FunctionCall {
2941 name: "dangerous".to_string(),
2942 arguments: "{}".to_string(),
2943 },
2944 };
2945 let runtime = AgentRuntime::builder()
2946 .register_model(profile())
2947 .register_agent(
2948 AgentDefinition::builder("agent")
2949 .description("agent")
2950 .build()
2951 .unwrap(),
2952 )
2953 .register_tool(dangerous_tool)
2954 .model_executor(Arc::new(MockModelExecutor::new(vec![
2955 response("", Some(vec![hallucinated_call])),
2956 response("done", None),
2957 ])))
2958 .build()
2959 .unwrap();
2960
2961 let result = runtime
2962 .run(AgentRunRequest::new("agent", "start"))
2963 .await
2964 .unwrap();
2965 assert_eq!(result.outcome, RunOutcome::Completed);
2966 assert_eq!(executions.load(Ordering::SeqCst), 0);
2967 otherone_storage::localfile::clear_storage_root();
2968 }
2969
2970 #[tokio::test]
2971 async fn fatal_tool_errors_terminate_the_current_run() {
2972 let _guard = storage_guard().await;
2973 otherone_storage::localfile::set_storage_root(temp_storage());
2974 let fatal_tool = ToolRegistration::sync(
2975 Tool {
2976 tool_type: "function".to_string(),
2977 function: FunctionDefinition {
2978 name: "fatal_tool".to_string(),
2979 description: "fails fatally".to_string(),
2980 parameters: None,
2981 },
2982 },
2983 |_| -> String { panic!("fatal tool panic") },
2984 );
2985 let call = ToolCall {
2986 index: Some(0),
2987 id: "call_fatal".to_string(),
2988 call_type: "function".to_string(),
2989 function: otherone_ai::types::FunctionCall {
2990 name: "fatal_tool".to_string(),
2991 arguments: "{}".to_string(),
2992 },
2993 };
2994 let executor = Arc::new(MockModelExecutor::new(vec![response("", Some(vec![call]))]));
2995 let runtime = AgentRuntime::builder()
2996 .register_model(profile())
2997 .register_agent(
2998 AgentDefinition::builder("agent")
2999 .description("agent")
3000 .allow_tools(["fatal_tool"])
3001 .build()
3002 .unwrap(),
3003 )
3004 .register_tool(fatal_tool)
3005 .model_executor(executor.clone())
3006 .build()
3007 .unwrap();
3008
3009 let result = runtime
3010 .run(AgentRunRequest::new("agent", "start"))
3011 .await
3012 .unwrap();
3013 assert!(matches!(
3014 &result.outcome,
3015 RunOutcome::Failed(AgentFailure { code, .. }) if code == "tool_error"
3016 ));
3017 assert_eq!(result.usage.model_calls, 1);
3018 assert_eq!(result.usage.tool_calls, 1);
3019 assert_eq!(executor.configs().await.len(), 1);
3020 otherone_storage::localfile::clear_storage_root();
3021 }
3022
3023 #[tokio::test]
3024 async fn denies_agent_call_cycles_without_starting_a_third_run() {
3025 let _guard = storage_guard().await;
3026 otherone_storage::localfile::set_storage_root(temp_storage());
3027 let runtime = AgentRuntime::builder()
3028 .register_model(profile())
3029 .register_agent(
3030 AgentDefinition::builder("agent_a")
3031 .description("A")
3032 .allow_agents(["agent_b"])
3033 .build()
3034 .unwrap(),
3035 )
3036 .register_agent(
3037 AgentDefinition::builder("agent_b")
3038 .description("B")
3039 .allow_agents(["agent_a"])
3040 .build()
3041 .unwrap(),
3042 )
3043 .model_executor(Arc::new(MockModelExecutor::new(vec![
3044 response("", Some(vec![agent_call("call_b", "agent_b", "go to B")])),
3045 response(
3046 "",
3047 Some(vec![agent_call("call_a", "agent_a", "go back to A")]),
3048 ),
3049 response("B handled cycle", None),
3050 response("A final", None),
3051 ])))
3052 .build()
3053 .unwrap();
3054
3055 let result = runtime
3056 .run(AgentRunRequest::new("agent_a", "start"))
3057 .await
3058 .unwrap();
3059 assert_eq!(result.outcome, RunOutcome::Completed);
3060 let records = runtime.list_run_tree(&result.root_run_id).await;
3061 assert_eq!(records.len(), 2);
3062 otherone_storage::localfile::clear_storage_root();
3063 }
3064
3065 #[tokio::test]
3066 async fn nested_call_does_not_deadlock_with_concurrency_one() {
3067 let _guard = storage_guard().await;
3068 otherone_storage::localfile::set_storage_root(temp_storage());
3069 let limits = RuntimeLimits {
3070 max_concurrent_model_calls: 1,
3071 max_concurrent_tool_calls: 1,
3072 ..RuntimeLimits::default()
3073 };
3074 let runtime = AgentRuntime::builder()
3075 .register_model(profile())
3076 .register_agent(
3077 AgentDefinition::builder("root")
3078 .description("root")
3079 .allow_agents(["worker"])
3080 .build()
3081 .unwrap(),
3082 )
3083 .register_agent(
3084 AgentDefinition::builder("worker")
3085 .description("worker")
3086 .build()
3087 .unwrap(),
3088 )
3089 .runtime_limits(limits)
3090 .model_executor(Arc::new(MockModelExecutor::new(vec![
3091 response("", Some(vec![agent_call("call_1", "worker", "work")])),
3092 response("worked", None),
3093 response("done", None),
3094 ])))
3095 .build()
3096 .unwrap();
3097
3098 let result = tokio::time::timeout(
3099 Duration::from_secs(2),
3100 runtime.run(AgentRunRequest::new("root", "start")),
3101 )
3102 .await
3103 .expect("nested Agent call deadlocked")
3104 .unwrap();
3105 assert_eq!(result.outcome, RunOutcome::Completed);
3106 otherone_storage::localfile::clear_storage_root();
3107 }
3108
3109 struct PendingModelExecutor;
3110
3111 #[async_trait]
3112 impl ModelExecutor for PendingModelExecutor {
3113 async fn stream(
3114 &self,
3115 _profile: &ModelProfile,
3116 _config: serde_json::Value,
3117 ) -> Result<ChatStream, AiError> {
3118 Ok(Box::pin(stream::pending()))
3119 }
3120 }
3121
3122 struct RootThenPendingModelExecutor {
3123 calls: TestAtomicUsize,
3124 }
3125
3126 #[async_trait]
3127 impl ModelExecutor for RootThenPendingModelExecutor {
3128 async fn stream(
3129 &self,
3130 _profile: &ModelProfile,
3131 _config: serde_json::Value,
3132 ) -> Result<ChatStream, AiError> {
3133 if self.calls.fetch_add(1, Ordering::SeqCst) == 0 {
3134 Ok(Box::pin(stream::iter(vec![Ok(response(
3135 "",
3136 Some(vec![agent_call("call_1", "worker", "wait")]),
3137 ))])))
3138 } else {
3139 Ok(Box::pin(stream::pending()))
3140 }
3141 }
3142 }
3143
3144 struct RootPendingThenInvalidJsonExecutor {
3145 calls: TestAtomicUsize,
3146 }
3147
3148 #[async_trait]
3149 impl ModelExecutor for RootPendingThenInvalidJsonExecutor {
3150 async fn stream(
3151 &self,
3152 _profile: &ModelProfile,
3153 _config: serde_json::Value,
3154 ) -> Result<ChatStream, AiError> {
3155 if self.calls.fetch_add(1, Ordering::SeqCst) == 0 {
3156 Ok(Box::pin(stream::pending()))
3157 } else {
3158 Ok(Box::pin(stream::iter(vec![Ok(response("not json", None))])))
3159 }
3160 }
3161 }
3162
3163 struct GatedModelExecutor {
3164 calls: TestAtomicUsize,
3165 release: Arc<tokio::sync::Semaphore>,
3166 }
3167
3168 #[async_trait]
3169 impl ModelExecutor for GatedModelExecutor {
3170 async fn stream(
3171 &self,
3172 _profile: &ModelProfile,
3173 _config: serde_json::Value,
3174 ) -> Result<ChatStream, AiError> {
3175 self.calls.fetch_add(1, Ordering::SeqCst);
3176 self.release
3177 .acquire()
3178 .await
3179 .map_err(|_| AiError::Other("gate closed".to_string()))?
3180 .forget();
3181 Ok(Box::pin(stream::iter(vec![Ok(response("once", None))])))
3182 }
3183 }
3184
3185 struct RootPendingWorkerGatedExecutor {
3186 calls: TestAtomicUsize,
3187 release: Arc<tokio::sync::Semaphore>,
3188 }
3189
3190 #[async_trait]
3191 impl ModelExecutor for RootPendingWorkerGatedExecutor {
3192 async fn stream(
3193 &self,
3194 _profile: &ModelProfile,
3195 _config: serde_json::Value,
3196 ) -> Result<ChatStream, AiError> {
3197 let call = self.calls.fetch_add(1, Ordering::SeqCst);
3198 if call == 0 {
3199 return Ok(Box::pin(stream::pending()));
3200 }
3201 self.release
3202 .acquire()
3203 .await
3204 .map_err(|_| AiError::Other("gate closed".to_string()))?
3205 .forget();
3206 Ok(Box::pin(stream::iter(vec![Ok(response("worked", None))])))
3207 }
3208 }
3209
3210 #[tokio::test]
3211 async fn cancellation_stops_an_active_root_run() {
3212 let _guard = storage_guard().await;
3213 otherone_storage::localfile::set_storage_root(temp_storage());
3214 let runtime = AgentRuntime::builder()
3215 .register_model(profile())
3216 .register_agent(
3217 AgentDefinition::builder("agent")
3218 .description("agent")
3219 .build()
3220 .unwrap(),
3221 )
3222 .model_executor(Arc::new(PendingModelExecutor))
3223 .build()
3224 .unwrap();
3225 let handle = runtime
3226 .start(AgentRunRequest::new("agent", "wait"))
3227 .await
3228 .unwrap();
3229 handle.commands.cancel().await.unwrap();
3230 let result = tokio::time::timeout(Duration::from_secs(2), handle.result())
3231 .await
3232 .expect("cancelled run did not finish")
3233 .unwrap();
3234 assert_eq!(result.outcome, RunOutcome::Cancelled);
3235 otherone_storage::localfile::clear_storage_root();
3236 }
3237
3238 #[tokio::test]
3239 async fn request_root_timeout_tightens_the_runtime_deadline() {
3240 let _guard = storage_guard().await;
3241 otherone_storage::localfile::set_storage_root(temp_storage());
3242 let runtime = AgentRuntime::builder()
3243 .register_model(profile())
3244 .register_agent(
3245 AgentDefinition::builder("agent")
3246 .description("agent")
3247 .build()
3248 .unwrap(),
3249 )
3250 .model_executor(Arc::new(PendingModelExecutor))
3251 .build()
3252 .unwrap();
3253 let request = AgentRunRequest::new("agent", "wait").limits(RunLimitOverrides {
3254 root_timeout: Some(Duration::from_millis(20)),
3255 ..Default::default()
3256 });
3257 let result = tokio::time::timeout(Duration::from_secs(2), runtime.run(request))
3258 .await
3259 .expect("request root timeout was not applied")
3260 .unwrap();
3261 assert_eq!(result.outcome, RunOutcome::TimedOut);
3262 otherone_storage::localfile::clear_storage_root();
3263 }
3264
3265 #[tokio::test]
3266 async fn root_cancellation_finishes_and_unregisters_active_children() {
3267 let _guard = storage_guard().await;
3268 otherone_storage::localfile::set_storage_root(temp_storage());
3269 let runtime = AgentRuntime::builder()
3270 .register_model(profile())
3271 .register_agent(
3272 AgentDefinition::builder("root")
3273 .description("root")
3274 .allow_agents(["worker"])
3275 .build()
3276 .unwrap(),
3277 )
3278 .register_agent(
3279 AgentDefinition::builder("worker")
3280 .description("worker")
3281 .build()
3282 .unwrap(),
3283 )
3284 .model_executor(Arc::new(RootThenPendingModelExecutor {
3285 calls: TestAtomicUsize::new(0),
3286 }))
3287 .build()
3288 .unwrap();
3289 let handle = runtime
3290 .start(AgentRunRequest::new("root", "start"))
3291 .await
3292 .unwrap();
3293 let root_run_id = handle.run_id.clone();
3294 let child_run_id = tokio::time::timeout(Duration::from_secs(2), async {
3295 loop {
3296 if let Some(record) = runtime
3297 .list_run_tree(&root_run_id)
3298 .await
3299 .into_iter()
3300 .find(|record| record.depth == 1)
3301 {
3302 break record.run_id;
3303 }
3304 tokio::task::yield_now().await;
3305 }
3306 })
3307 .await
3308 .expect("child run did not start");
3309
3310 handle.commands.cancel().await.unwrap();
3311 let root_result = tokio::time::timeout(Duration::from_secs(2), handle.result())
3312 .await
3313 .expect("cancelled root run did not finish")
3314 .unwrap();
3315 assert_eq!(root_result.outcome, RunOutcome::Cancelled);
3316
3317 tokio::time::timeout(Duration::from_secs(2), async {
3318 loop {
3319 if runtime
3320 .get_run(&child_run_id)
3321 .await
3322 .is_some_and(|record| matches!(record.status, RunStatus::Cancelled))
3323 {
3324 break;
3325 }
3326 tokio::task::yield_now().await;
3327 }
3328 })
3329 .await
3330 .expect("cancelled child run did not finish");
3331 assert!(runtime.cancel(&child_run_id).await.is_err());
3332 otherone_storage::localfile::clear_storage_root();
3333 }
3334
3335 #[tokio::test]
3336 async fn invalid_child_result_contract_updates_the_child_run_status() {
3337 let _guard = storage_guard().await;
3338 otherone_storage::localfile::set_storage_root(temp_storage());
3339 let executor = Arc::new(RootPendingThenInvalidJsonExecutor {
3340 calls: TestAtomicUsize::new(0),
3341 });
3342 let runtime = AgentRuntime::builder()
3343 .register_model(profile())
3344 .register_agent(
3345 AgentDefinition::builder("root")
3346 .description("root")
3347 .allow_agents(["worker"])
3348 .build()
3349 .unwrap(),
3350 )
3351 .register_agent(
3352 AgentDefinition::builder("worker")
3353 .description("worker")
3354 .build()
3355 .unwrap(),
3356 )
3357 .model_executor(executor.clone())
3358 .build()
3359 .unwrap();
3360 let handle = runtime
3361 .start(AgentRunRequest::new("root", "wait"))
3362 .await
3363 .unwrap();
3364 tokio::time::timeout(Duration::from_secs(2), async {
3365 while executor.calls.load(Ordering::SeqCst) == 0 {
3366 tokio::task::yield_now().await;
3367 }
3368 })
3369 .await
3370 .expect("root model call did not start");
3371
3372 let call_result = runtime
3373 .call_agent(
3374 &handle.run_id,
3375 AgentCallId::new("trusted-call").unwrap(),
3376 AgentCallRequest::new("worker", "return JSON")
3377 .result_contract(ResultContract::Json { schema: None }),
3378 )
3379 .await
3380 .unwrap();
3381 assert!(matches!(
3382 &call_result.outcome,
3383 RunOutcome::Failed(AgentFailure { code, .. }) if code == "invalid_agent_output"
3384 ));
3385 let child = runtime
3386 .get_run(call_result.run_id.as_ref().unwrap())
3387 .await
3388 .unwrap();
3389 assert!(matches!(child.status, RunStatus::Failed));
3390 assert!(matches!(
3391 &child.failure,
3392 Some(AgentFailure { code, .. }) if code == "invalid_agent_output"
3393 ));
3394
3395 handle.commands.cancel().await.unwrap();
3396 let _ = handle.result().await.unwrap();
3397 otherone_storage::localfile::clear_storage_root();
3398 }
3399
3400 #[tokio::test]
3401 async fn session_writer_lock_is_shared_across_runtimes() {
3402 let _guard = storage_guard().await;
3403 otherone_storage::localfile::set_storage_root(temp_storage());
3404 let build_runtime = || {
3405 AgentRuntime::builder()
3406 .register_model(profile())
3407 .register_agent(
3408 AgentDefinition::builder("agent")
3409 .description("agent")
3410 .build()
3411 .unwrap(),
3412 )
3413 .model_executor(Arc::new(PendingModelExecutor))
3414 .build()
3415 .unwrap()
3416 };
3417 let first_runtime = build_runtime();
3418 let second_runtime = build_runtime();
3419 let request = AgentRunRequest::new("agent", "wait")
3420 .session(super::super::types::SessionTarget::local().session_id("shared-session"));
3421 let first = first_runtime.start(request.clone()).await.unwrap();
3422 let second = second_runtime.start(request).await;
3423 assert!(matches!(second, Err(AgentError::SessionBusy(_))));
3424 first.commands.cancel().await.unwrap();
3425 let _ = first.result().await.unwrap();
3426 otherone_storage::localfile::clear_storage_root();
3427 }
3428
3429 #[tokio::test]
3430 async fn root_idempotency_returns_the_original_result() {
3431 let _guard = storage_guard().await;
3432 otherone_storage::localfile::set_storage_root(temp_storage());
3433 let executor = Arc::new(MockModelExecutor::new(vec![response("once", None)]));
3434 let runtime = AgentRuntime::builder()
3435 .register_model(profile())
3436 .register_agent(
3437 AgentDefinition::builder("agent")
3438 .description("agent")
3439 .build()
3440 .unwrap(),
3441 )
3442 .model_executor(executor.clone())
3443 .build()
3444 .unwrap();
3445 let request = AgentRunRequest::new("agent", "hello").idempotency_key("request-1");
3446 let first = runtime.run(request.clone()).await.unwrap();
3447 let second = runtime.run(request).await.unwrap();
3448 assert_eq!(first.run_id, second.run_id);
3449 assert_eq!(executor.configs().await.len(), 1);
3450 otherone_storage::localfile::clear_storage_root();
3451 }
3452
3453 #[tokio::test]
3454 async fn root_idempotency_survives_a_cancelled_waiter() {
3455 let _guard = storage_guard().await;
3456 otherone_storage::localfile::set_storage_root(temp_storage());
3457 let release = Arc::new(tokio::sync::Semaphore::new(0));
3458 let executor = Arc::new(GatedModelExecutor {
3459 calls: TestAtomicUsize::new(0),
3460 release: Arc::clone(&release),
3461 });
3462 let runtime = AgentRuntime::builder()
3463 .register_model(profile())
3464 .register_agent(
3465 AgentDefinition::builder("agent")
3466 .description("agent")
3467 .build()
3468 .unwrap(),
3469 )
3470 .model_executor(executor.clone())
3471 .build()
3472 .unwrap();
3473 let request = AgentRunRequest::new("agent", "hello").idempotency_key("request-1");
3474 let first_runtime = runtime.clone();
3475 let first_request = request.clone();
3476 let first_waiter = tokio::spawn(async move { first_runtime.run(first_request).await });
3477 tokio::time::timeout(Duration::from_secs(2), async {
3478 while executor.calls.load(Ordering::SeqCst) == 0 {
3479 tokio::task::yield_now().await;
3480 }
3481 })
3482 .await
3483 .expect("idempotent producer did not start");
3484 first_waiter.abort();
3485 let _ = first_waiter.await;
3486 release.add_permits(1);
3487
3488 let result = tokio::time::timeout(Duration::from_secs(2), runtime.run(request))
3489 .await
3490 .expect("replacement waiter did not receive the cached result")
3491 .unwrap();
3492 assert_eq!(result.output, Some(AgentOutput::Text("once".to_string())));
3493 assert_eq!(executor.calls.load(Ordering::SeqCst), 1);
3494 otherone_storage::localfile::clear_storage_root();
3495 }
3496
3497 #[tokio::test]
3498 async fn repeated_agent_call_id_executes_child_once() {
3499 let _guard = storage_guard().await;
3500 otherone_storage::localfile::set_storage_root(temp_storage());
3501 let executor = Arc::new(MockModelExecutor::new(vec![
3502 response("", Some(vec![agent_call("same_call", "worker", "work")])),
3503 response("worked once", None),
3504 response("", Some(vec![agent_call("same_call", "worker", "work")])),
3505 response("done", None),
3506 ]));
3507 let runtime = AgentRuntime::builder()
3508 .register_model(profile())
3509 .register_agent(
3510 AgentDefinition::builder("root")
3511 .description("root")
3512 .allow_agents(["worker"])
3513 .build()
3514 .unwrap(),
3515 )
3516 .register_agent(
3517 AgentDefinition::builder("worker")
3518 .description("worker")
3519 .build()
3520 .unwrap(),
3521 )
3522 .model_executor(executor.clone())
3523 .build()
3524 .unwrap();
3525
3526 let result = runtime
3527 .run(AgentRunRequest::new("root", "start"))
3528 .await
3529 .unwrap();
3530 assert_eq!(runtime.list_run_tree(&result.root_run_id).await.len(), 2);
3531 assert_eq!(executor.configs().await.len(), 4);
3532 otherone_storage::localfile::clear_storage_root();
3533 }
3534
3535 #[tokio::test]
3536 async fn agent_call_idempotency_survives_a_cancelled_waiter() {
3537 let _guard = storage_guard().await;
3538 otherone_storage::localfile::set_storage_root(temp_storage());
3539 let release = Arc::new(tokio::sync::Semaphore::new(0));
3540 let executor = Arc::new(RootPendingWorkerGatedExecutor {
3541 calls: TestAtomicUsize::new(0),
3542 release: Arc::clone(&release),
3543 });
3544 let runtime = AgentRuntime::builder()
3545 .register_model(profile())
3546 .register_agent(
3547 AgentDefinition::builder("root")
3548 .description("root")
3549 .allow_agents(["worker"])
3550 .build()
3551 .unwrap(),
3552 )
3553 .register_agent(
3554 AgentDefinition::builder("worker")
3555 .description("worker")
3556 .build()
3557 .unwrap(),
3558 )
3559 .model_executor(executor.clone())
3560 .build()
3561 .unwrap();
3562 let handle = runtime
3563 .start(AgentRunRequest::new("root", "wait"))
3564 .await
3565 .unwrap();
3566 tokio::time::timeout(Duration::from_secs(2), async {
3567 while executor.calls.load(Ordering::SeqCst) == 0 {
3568 tokio::task::yield_now().await;
3569 }
3570 })
3571 .await
3572 .expect("root model call did not start");
3573
3574 let first_runtime = runtime.clone();
3575 let caller_run_id = handle.run_id.clone();
3576 let first_waiter = tokio::spawn(async move {
3577 first_runtime
3578 .call_agent(
3579 &caller_run_id,
3580 AgentCallId::new("stable-call").unwrap(),
3581 AgentCallRequest::new("worker", "work"),
3582 )
3583 .await
3584 });
3585 tokio::time::timeout(Duration::from_secs(2), async {
3586 while executor.calls.load(Ordering::SeqCst) < 2 {
3587 tokio::task::yield_now().await;
3588 }
3589 })
3590 .await
3591 .expect("child model call did not start");
3592 first_waiter.abort();
3593 let _ = first_waiter.await;
3594 release.add_permits(1);
3595
3596 let result = tokio::time::timeout(
3597 Duration::from_secs(2),
3598 runtime.call_agent(
3599 &handle.run_id,
3600 AgentCallId::new("stable-call").unwrap(),
3601 AgentCallRequest::new("worker", "work"),
3602 ),
3603 )
3604 .await
3605 .expect("replacement Agent-call waiter did not receive the result")
3606 .unwrap();
3607 assert_eq!(result.output, Some(AgentOutput::Text("worked".to_string())));
3608 assert_eq!(executor.calls.load(Ordering::SeqCst), 2);
3609 assert_eq!(runtime.list_run_tree(&handle.run_id).await.len(), 2);
3610
3611 handle.commands.cancel().await.unwrap();
3612 let _ = handle.result().await.unwrap();
3613 otherone_storage::localfile::clear_storage_root();
3614 }
3615
3616 #[tokio::test]
3617 async fn event_stream_contains_parent_and_child_lineage() {
3618 let _guard = storage_guard().await;
3619 otherone_storage::localfile::set_storage_root(temp_storage());
3620 let runtime = AgentRuntime::builder()
3621 .register_model(profile())
3622 .register_agent(
3623 AgentDefinition::builder("root")
3624 .description("root")
3625 .allow_agents(["worker"])
3626 .build()
3627 .unwrap(),
3628 )
3629 .register_agent(
3630 AgentDefinition::builder("worker")
3631 .description("worker")
3632 .build()
3633 .unwrap(),
3634 )
3635 .model_executor(Arc::new(MockModelExecutor::new(vec![
3636 response("", Some(vec![agent_call("call_1", "worker", "work")])),
3637 response("worked", None),
3638 response("done", None),
3639 ])))
3640 .build()
3641 .unwrap();
3642 let mut handle = runtime
3643 .start(AgentRunRequest::new("root", "start"))
3644 .await
3645 .unwrap();
3646 let mut events = Vec::new();
3647 while let Some(event) = handle.events.recv().await {
3648 events.push(event);
3649 }
3650 let result = handle.result().await.unwrap();
3651 assert!(events.iter().any(|event| event.run_id == result.run_id));
3652 assert!(events.iter().any(|event| {
3653 event.parent_run_id.as_ref() == Some(&result.run_id)
3654 && event.agent_id.as_str() == "worker"
3655 }));
3656 let mut sequences = events
3657 .iter()
3658 .map(|event| event.sequence)
3659 .collect::<Vec<_>>();
3660 sequences.sort_unstable();
3661 sequences.dedup();
3662 assert_eq!(sequences.len(), events.len());
3663 otherone_storage::localfile::clear_storage_root();
3664 }
3665}