Skip to main content

otherone_agent/multi_agent/
types.rs

1use std::collections::BTreeSet;
2use std::fmt;
3use std::time::Duration;
4
5use otherone_ai::types::{Message, ProviderType, ToolChoice};
6use otherone_storage::types::{AttributeBag, DatabaseConfig, RuntimeContext};
7use serde::{Deserialize, Serialize};
8
9use crate::error::AgentError;
10use crate::types::{ContextLoadType, StorageType};
11
12fn validate_id(value: &str, kind: &str) -> Result<(), AgentError> {
13    if value.is_empty() || value.len() > 64 {
14        return Err(AgentError::InvalidConfiguration(format!(
15            "{kind} must contain 1-64 characters"
16        )));
17    }
18    let mut chars = value.chars();
19    if !chars.next().is_some_and(|ch| ch.is_ascii_lowercase())
20        || !chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || ch == '-')
21    {
22        return Err(AgentError::InvalidConfiguration(format!(
23            "invalid {kind} '{value}': expected [a-z][a-z0-9_-]{{0,63}}"
24        )));
25    }
26    Ok(())
27}
28
29macro_rules! id_type {
30    ($name:ident, $kind:literal) => {
31        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
32        #[serde(transparent)]
33        pub struct $name(String);
34
35        impl $name {
36            pub fn new(value: impl Into<String>) -> Result<Self, AgentError> {
37                let value = value.into();
38                validate_id(&value, $kind)?;
39                Ok(Self(value))
40            }
41
42            pub fn as_str(&self) -> &str {
43                &self.0
44            }
45
46            pub(crate) fn unchecked(value: impl Into<String>) -> Self {
47                Self(value.into())
48            }
49
50            pub(crate) fn validate(&self) -> Result<(), AgentError> {
51                validate_id(&self.0, $kind)
52            }
53        }
54
55        impl fmt::Display for $name {
56            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57                formatter.write_str(&self.0)
58            }
59        }
60
61        impl TryFrom<&str> for $name {
62            type Error = AgentError;
63
64            fn try_from(value: &str) -> Result<Self, Self::Error> {
65                Self::new(value)
66            }
67        }
68
69        impl TryFrom<String> for $name {
70            type Error = AgentError;
71
72            fn try_from(value: String) -> Result<Self, Self::Error> {
73                Self::new(value)
74            }
75        }
76    };
77}
78
79id_type!(AgentId, "agent id");
80id_type!(ModelProfileId, "model profile id");
81
82#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
83#[serde(transparent)]
84pub struct RunId(String);
85
86impl RunId {
87    pub fn new() -> Self {
88        Self(uuid::Uuid::new_v4().to_string())
89    }
90
91    pub fn from_string(value: impl Into<String>) -> Result<Self, AgentError> {
92        let value = value.into();
93        if value.trim().is_empty() {
94            return Err(AgentError::InvalidConfiguration(
95                "run id is required".to_string(),
96            ));
97        }
98        Ok(Self(value))
99    }
100
101    pub fn as_str(&self) -> &str {
102        &self.0
103    }
104}
105
106impl Default for RunId {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl fmt::Display for RunId {
113    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114        formatter.write_str(&self.0)
115    }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
119#[serde(transparent)]
120pub struct AgentCallId(String);
121
122impl AgentCallId {
123    pub fn new(value: impl Into<String>) -> Result<Self, AgentError> {
124        let value = value.into();
125        if value.trim().is_empty() || value.len() > 256 {
126            return Err(AgentError::InvalidConfiguration(
127                "agent call id must contain 1-256 characters".to_string(),
128            ));
129        }
130        Ok(Self(value))
131    }
132
133    pub fn generated() -> Self {
134        Self(uuid::Uuid::new_v4().to_string())
135    }
136
137    pub fn as_str(&self) -> &str {
138        &self.0
139    }
140}
141
142impl fmt::Display for AgentCallId {
143    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144        formatter.write_str(&self.0)
145    }
146}
147
148#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150#[serde(bound(serialize = "T: Serialize", deserialize = "T: Ord + Deserialize<'de>"))]
151pub enum AccessPolicy<T> {
152    #[default]
153    None,
154    Allow(BTreeSet<T>),
155    All,
156}
157
158impl<T> AccessPolicy<T>
159where
160    T: Ord,
161{
162    pub fn allows(&self, value: &T) -> bool {
163        match self {
164            Self::None => false,
165            Self::Allow(values) => values.contains(value),
166            Self::All => true,
167        }
168    }
169
170    pub fn allowed_values(&self) -> Option<&BTreeSet<T>> {
171        match self {
172            Self::Allow(values) => Some(values),
173            Self::None | Self::All => None,
174        }
175    }
176}
177
178pub type ToolAccessPolicy = AccessPolicy<String>;
179pub type SkillAccessPolicy = AccessPolicy<String>;
180pub type AgentAccessPolicy = AccessPolicy<AgentId>;
181
182#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum MemoryPolicy {
185    #[default]
186    Disabled,
187    ReadOnlyShared,
188    ReadWriteShared,
189    PrivateAgent,
190}
191
192#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum ContextTransferPolicy {
195    #[default]
196    ExplicitOnly,
197    ParentSummary {
198        max_tokens: u32,
199    },
200    ParentWindow {
201        max_tokens: u32,
202    },
203}
204
205#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "snake_case")]
207pub enum ModelSelector {
208    #[default]
209    RuntimeDefault,
210    Named(ModelProfileId),
211    InheritCaller,
212}
213
214#[derive(Debug, Clone, Default, Serialize, Deserialize)]
215pub struct ModelOverrides {
216    pub context_length: Option<u32>,
217    pub max_tokens: Option<u32>,
218    pub temperature: Option<f32>,
219    pub top_p: Option<f32>,
220    pub tool_choice: Option<ToolChoice>,
221    pub parallel_tool_calls: Option<bool>,
222    pub other: Option<serde_json::Value>,
223}
224
225#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226pub struct AgentLimits {
227    pub max_iterations: Option<u32>,
228    pub timeout_millis: Option<u64>,
229    pub max_result_bytes: Option<usize>,
230}
231
232impl AgentLimits {
233    pub fn max_iterations(mut self, value: u32) -> Self {
234        self.max_iterations = Some(value);
235        self
236    }
237
238    pub fn timeout(mut self, value: Duration) -> Self {
239        self.timeout_millis = Some(value.as_millis().min(u64::MAX as u128) as u64);
240        self
241    }
242
243    pub fn max_result_bytes(mut self, value: usize) -> Self {
244        self.max_result_bytes = Some(value);
245        self
246    }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
250#[non_exhaustive]
251pub struct AgentDefinition {
252    pub id: AgentId,
253    pub version: u64,
254    pub description: String,
255    pub system_prompt: String,
256    pub model: ModelSelector,
257    pub model_overrides: ModelOverrides,
258    pub tools: ToolAccessPolicy,
259    pub skills: SkillAccessPolicy,
260    pub callable_agents: AgentAccessPolicy,
261    pub memory: MemoryPolicy,
262    pub context: ContextTransferPolicy,
263    pub limits: AgentLimits,
264    pub metadata: AttributeBag,
265}
266
267impl AgentDefinition {
268    pub fn builder(id: impl Into<String>) -> AgentDefinitionBuilder {
269        AgentDefinitionBuilder::new(id)
270    }
271
272    pub(crate) fn validate(&self) -> Result<(), AgentError> {
273        self.id.validate()?;
274        if self.version == 0 {
275            return Err(AgentError::InvalidConfiguration(format!(
276                "agent '{}' version must be greater than zero",
277                self.id
278            )));
279        }
280        let description = self.description.trim();
281        if description.is_empty() || description.len() > 1024 {
282            return Err(AgentError::InvalidConfiguration(format!(
283                "agent '{}' description must contain 1-1024 bytes",
284                self.id
285            )));
286        }
287        if self.system_prompt.len() > 128 * 1024 {
288            return Err(AgentError::InvalidConfiguration(format!(
289                "agent '{}' system prompt is too large",
290                self.id
291            )));
292        }
293        if self.limits.max_iterations == Some(0) {
294            return Err(AgentError::InvalidConfiguration(format!(
295                "agent '{}' max_iterations must be greater than zero",
296                self.id
297            )));
298        }
299        if self.limits.timeout_millis == Some(0) || self.limits.max_result_bytes == Some(0) {
300            return Err(AgentError::InvalidConfiguration(format!(
301                "agent '{}' timeout and result limits must be greater than zero",
302                self.id
303            )));
304        }
305        validate_model_values(
306            &format!("agent '{}' model overrides", self.id),
307            self.model_overrides.context_length,
308            self.model_overrides.max_tokens,
309            self.model_overrides.temperature,
310            self.model_overrides.top_p,
311            self.model_overrides.other.as_ref(),
312        )?;
313        for target in self.callable_agents.allowed_values().into_iter().flatten() {
314            target.validate()?;
315        }
316        Ok(())
317    }
318}
319
320pub struct AgentDefinitionBuilder {
321    definition: AgentDefinition,
322}
323
324impl AgentDefinitionBuilder {
325    fn new(id: impl Into<String>) -> Self {
326        Self {
327            definition: AgentDefinition {
328                id: AgentId::unchecked(id),
329                version: 1,
330                description: String::new(),
331                system_prompt: String::new(),
332                model: ModelSelector::RuntimeDefault,
333                model_overrides: ModelOverrides::default(),
334                tools: ToolAccessPolicy::None,
335                skills: SkillAccessPolicy::None,
336                callable_agents: AgentAccessPolicy::None,
337                memory: MemoryPolicy::Disabled,
338                context: ContextTransferPolicy::ExplicitOnly,
339                limits: AgentLimits::default(),
340                metadata: AttributeBag::new(),
341            },
342        }
343    }
344
345    pub fn version(mut self, version: u64) -> Self {
346        self.definition.version = version;
347        self
348    }
349
350    pub fn description(mut self, description: impl Into<String>) -> Self {
351        self.definition.description = description.into();
352        self
353    }
354
355    pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
356        self.definition.system_prompt = system_prompt.into();
357        self
358    }
359
360    pub fn model(mut self, model: ModelSelector) -> Self {
361        self.definition.model = model;
362        self
363    }
364
365    pub fn model_overrides(mut self, overrides: ModelOverrides) -> Self {
366        self.definition.model_overrides = overrides;
367        self
368    }
369
370    pub fn tools(mut self, tools: ToolAccessPolicy) -> Self {
371        self.definition.tools = tools;
372        self
373    }
374
375    pub fn allow_tools<I, S>(mut self, tools: I) -> Self
376    where
377        I: IntoIterator<Item = S>,
378        S: Into<String>,
379    {
380        self.definition.tools =
381            ToolAccessPolicy::Allow(tools.into_iter().map(Into::into).collect());
382        self
383    }
384
385    pub fn skills(mut self, skills: SkillAccessPolicy) -> Self {
386        self.definition.skills = skills;
387        self
388    }
389
390    pub fn allow_skills<I, S>(mut self, skills: I) -> Self
391    where
392        I: IntoIterator<Item = S>,
393        S: Into<String>,
394    {
395        self.definition.skills =
396            SkillAccessPolicy::Allow(skills.into_iter().map(Into::into).collect());
397        self
398    }
399
400    pub fn callable_agents(mut self, agents: AgentAccessPolicy) -> Self {
401        self.definition.callable_agents = agents;
402        self
403    }
404
405    pub fn allow_agents<I, S>(mut self, agents: I) -> Self
406    where
407        I: IntoIterator<Item = S>,
408        S: Into<String>,
409    {
410        self.definition.callable_agents = AgentAccessPolicy::Allow(
411            agents
412                .into_iter()
413                .map(|value| AgentId::unchecked(value))
414                .collect(),
415        );
416        self
417    }
418
419    pub fn memory(mut self, memory: MemoryPolicy) -> Self {
420        self.definition.memory = memory;
421        self
422    }
423
424    pub fn context(mut self, context: ContextTransferPolicy) -> Self {
425        self.definition.context = context;
426        self
427    }
428
429    pub fn limits(mut self, limits: AgentLimits) -> Self {
430        self.definition.limits = limits;
431        self
432    }
433
434    pub fn metadata(mut self, metadata: AttributeBag) -> Self {
435        self.definition.metadata = metadata;
436        self
437    }
438
439    pub fn build(self) -> Result<AgentDefinition, AgentError> {
440        self.definition.validate()?;
441        Ok(self.definition)
442    }
443}
444
445#[derive(Clone)]
446pub struct ModelProfile {
447    pub id: ModelProfileId,
448    pub provider: ProviderType,
449    pub base_url: String,
450    pub model: String,
451    pub context_length: Option<u32>,
452    pub max_tokens: Option<u32>,
453    pub temperature: Option<f32>,
454    pub top_p: Option<f32>,
455    pub other: Option<serde_json::Value>,
456    pub(crate) api_key: String,
457}
458
459impl fmt::Debug for ModelProfile {
460    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
461        formatter
462            .debug_struct("ModelProfile")
463            .field("id", &self.id)
464            .field("provider", &self.provider)
465            .field("base_url", &self.base_url)
466            .field("model", &self.model)
467            .field("api_key", &"[REDACTED]")
468            .finish_non_exhaustive()
469    }
470}
471
472impl ModelProfile {
473    pub fn builder(
474        id: impl Into<String>,
475        provider: ProviderType,
476        model: impl Into<String>,
477    ) -> ModelProfileBuilder {
478        ModelProfileBuilder {
479            profile: Self {
480                id: ModelProfileId::unchecked(id),
481                provider,
482                api_key: String::new(),
483                base_url: String::new(),
484                model: model.into(),
485                context_length: None,
486                max_tokens: None,
487                temperature: None,
488                top_p: None,
489                other: None,
490            },
491        }
492    }
493
494    pub fn api_key(&self) -> &str {
495        &self.api_key
496    }
497
498    pub(crate) fn validate(&self) -> Result<(), AgentError> {
499        self.id.validate()?;
500        if self.api_key.trim().is_empty() {
501            return Err(AgentError::InvalidConfiguration(format!(
502                "model profile '{}' api_key is required",
503                self.id
504            )));
505        }
506        if self.base_url.trim().is_empty() {
507            return Err(AgentError::InvalidConfiguration(format!(
508                "model profile '{}' base_url is required",
509                self.id
510            )));
511        }
512        if self.model.trim().is_empty() {
513            return Err(AgentError::InvalidConfiguration(format!(
514                "model profile '{}' model is required",
515                self.id
516            )));
517        }
518        validate_model_values(
519            &format!("model profile '{}'", self.id),
520            self.context_length,
521            self.max_tokens,
522            self.temperature,
523            self.top_p,
524            self.other.as_ref(),
525        )?;
526        Ok(())
527    }
528}
529
530pub struct ModelProfileBuilder {
531    profile: ModelProfile,
532}
533
534impl ModelProfileBuilder {
535    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
536        self.profile.api_key = api_key.into();
537        self
538    }
539
540    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
541        self.profile.base_url = base_url.into();
542        self
543    }
544
545    pub fn context_length(mut self, value: u32) -> Self {
546        self.profile.context_length = Some(value);
547        self
548    }
549
550    pub fn max_tokens(mut self, value: u32) -> Self {
551        self.profile.max_tokens = Some(value);
552        self
553    }
554
555    pub fn temperature(mut self, value: f32) -> Self {
556        self.profile.temperature = Some(value);
557        self
558    }
559
560    pub fn top_p(mut self, value: f32) -> Self {
561        self.profile.top_p = Some(value);
562        self
563    }
564
565    pub fn other(mut self, value: serde_json::Value) -> Self {
566        self.profile.other = Some(value);
567        self
568    }
569
570    pub fn build(self) -> Result<ModelProfile, AgentError> {
571        self.profile.validate()?;
572        Ok(self.profile)
573    }
574}
575
576#[derive(Debug, Clone)]
577pub struct RuntimeLimits {
578    pub max_depth: usize,
579    pub max_total_runs: usize,
580    pub max_children_per_run: usize,
581    pub max_concurrent_model_calls: usize,
582    pub max_concurrent_tool_calls: usize,
583    pub max_iterations_per_run: u32,
584    pub max_model_calls: u32,
585    pub max_tool_calls: u32,
586    pub max_agent_calls: u32,
587    pub max_context_transfer_bytes: usize,
588    pub max_result_bytes: usize,
589    pub run_timeout: Duration,
590    pub root_timeout: Duration,
591    pub token_budget: Option<u64>,
592}
593
594impl RuntimeLimits {
595    pub fn safe_defaults() -> Self {
596        Self::default()
597    }
598
599    pub(crate) fn validate(&self) -> Result<(), AgentError> {
600        if self.max_total_runs == 0
601            || self.max_concurrent_model_calls == 0
602            || self.max_concurrent_tool_calls == 0
603            || self.max_iterations_per_run == 0
604            || self.max_model_calls == 0
605            || self.max_tool_calls == 0
606            || self.max_agent_calls == 0
607            || self.max_context_transfer_bytes == 0
608            || self.max_result_bytes == 0
609            || self.run_timeout.is_zero()
610            || self.root_timeout.is_zero()
611            || self.token_budget == Some(0)
612        {
613            return Err(AgentError::InvalidConfiguration(
614                "runtime limits must be greater than zero".to_string(),
615            ));
616        }
617        Ok(())
618    }
619}
620
621fn validate_model_values(
622    scope: &str,
623    context_length: Option<u32>,
624    max_tokens: Option<u32>,
625    temperature: Option<f32>,
626    top_p: Option<f32>,
627    other: Option<&serde_json::Value>,
628) -> Result<(), AgentError> {
629    if context_length == Some(0) || max_tokens == Some(0) {
630        return Err(AgentError::InvalidConfiguration(format!(
631            "{scope} token limits must be greater than zero"
632        )));
633    }
634    if temperature.is_some_and(|value| !value.is_finite()) {
635        return Err(AgentError::InvalidConfiguration(format!(
636            "{scope} temperature must be finite"
637        )));
638    }
639    if top_p.is_some_and(|value| !value.is_finite() || !(0.0..=1.0).contains(&value)) {
640        return Err(AgentError::InvalidConfiguration(format!(
641            "{scope} top_p must be between 0 and 1"
642        )));
643    }
644    if other.is_some_and(|value| !value.is_object()) {
645        return Err(AgentError::InvalidConfiguration(format!(
646            "{scope} other must be a JSON object"
647        )));
648    }
649    Ok(())
650}
651
652impl Default for RuntimeLimits {
653    fn default() -> Self {
654        Self {
655            max_depth: 3,
656            max_total_runs: 32,
657            max_children_per_run: 8,
658            max_concurrent_model_calls: 4,
659            max_concurrent_tool_calls: 8,
660            max_iterations_per_run: 12,
661            max_model_calls: 64,
662            max_tool_calls: 128,
663            max_agent_calls: 32,
664            max_context_transfer_bytes: 64 * 1024,
665            max_result_bytes: 256 * 1024,
666            run_timeout: Duration::from_secs(5 * 60),
667            root_timeout: Duration::from_secs(15 * 60),
668            token_budget: None,
669        }
670    }
671}
672
673#[derive(Debug, Clone, Default)]
674pub struct RunLimitOverrides {
675    pub max_depth: Option<usize>,
676    pub max_total_runs: Option<usize>,
677    pub max_iterations_per_run: Option<u32>,
678    pub max_result_bytes: Option<usize>,
679    pub root_timeout: Option<Duration>,
680    pub token_budget: Option<u64>,
681}
682
683#[derive(Debug, Clone)]
684pub struct SessionTarget {
685    pub session_id: Option<String>,
686    pub context_load_type: ContextLoadType,
687    pub storage_type: StorageType,
688    pub database_config: Option<DatabaseConfig>,
689    pub context_window: u32,
690    pub threshold_percentage: Option<f32>,
691}
692
693impl SessionTarget {
694    pub fn local() -> Self {
695        Self::default()
696    }
697
698    pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
699        self.session_id = Some(session_id.into());
700        self
701    }
702
703    pub fn context_window(mut self, context_window: u32) -> Self {
704        self.context_window = context_window;
705        self
706    }
707
708    pub fn threshold_percentage(mut self, threshold_percentage: f32) -> Self {
709        self.threshold_percentage = Some(threshold_percentage);
710        self
711    }
712
713    pub fn database(mut self, database_config: DatabaseConfig) -> Self {
714        self.context_load_type = ContextLoadType::Database;
715        self.storage_type = StorageType::Database;
716        self.database_config = Some(database_config);
717        self
718    }
719}
720
721impl Default for SessionTarget {
722    fn default() -> Self {
723        Self {
724            session_id: None,
725            context_load_type: ContextLoadType::LocalFile,
726            storage_type: StorageType::LocalFile,
727            database_config: None,
728            context_window: 8192,
729            threshold_percentage: Some(0.8),
730        }
731    }
732}
733
734#[derive(Debug, Clone)]
735pub enum AgentInput {
736    Text(String),
737    Messages(Vec<Message>),
738}
739
740impl From<String> for AgentInput {
741    fn from(value: String) -> Self {
742        Self::Text(value)
743    }
744}
745
746impl From<&str> for AgentInput {
747    fn from(value: &str) -> Self {
748        Self::Text(value.to_string())
749    }
750}
751
752#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
753pub enum EventVisibility {
754    #[default]
755    All,
756    LifecycleOnly,
757    RootOnly,
758}
759
760#[derive(Debug, Clone)]
761#[non_exhaustive]
762pub struct AgentRunRequest {
763    pub entry_agent: AgentId,
764    pub input: AgentInput,
765    pub session: SessionTarget,
766    pub runtime_context: Option<RuntimeContext>,
767    pub limits: Option<RunLimitOverrides>,
768    pub idempotency_key: Option<String>,
769    pub event_visibility: EventVisibility,
770    pub include_thinking_events: bool,
771    pub metadata: AttributeBag,
772}
773
774impl AgentRunRequest {
775    pub fn new(entry_agent: impl Into<String>, input: impl Into<AgentInput>) -> Self {
776        Self {
777            entry_agent: AgentId::unchecked(entry_agent),
778            input: input.into(),
779            session: SessionTarget::default(),
780            runtime_context: None,
781            limits: None,
782            idempotency_key: None,
783            event_visibility: EventVisibility::All,
784            include_thinking_events: false,
785            metadata: AttributeBag::new(),
786        }
787    }
788
789    pub fn session(mut self, session: SessionTarget) -> Self {
790        self.session = session;
791        self
792    }
793
794    pub fn runtime_context(mut self, runtime_context: RuntimeContext) -> Self {
795        self.runtime_context = Some(runtime_context);
796        self
797    }
798
799    pub fn limits(mut self, limits: RunLimitOverrides) -> Self {
800        self.limits = Some(limits);
801        self
802    }
803
804    pub fn idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self {
805        self.idempotency_key = Some(idempotency_key.into());
806        self
807    }
808
809    pub fn event_visibility(mut self, visibility: EventVisibility) -> Self {
810        self.event_visibility = visibility;
811        self
812    }
813
814    pub fn include_thinking_events(mut self, include: bool) -> Self {
815        self.include_thinking_events = include;
816        self
817    }
818
819    pub fn metadata(mut self, metadata: AttributeBag) -> Self {
820        self.metadata = metadata;
821        self
822    }
823}
824
825#[derive(Debug, Clone, Default, Serialize, Deserialize)]
826#[serde(rename_all = "snake_case")]
827pub enum ResultContract {
828    Text,
829    Json {
830        schema: Option<serde_json::Value>,
831    },
832    #[default]
833    Auto,
834}
835
836#[derive(Debug, Clone)]
837#[non_exhaustive]
838pub struct AgentCallRequest {
839    pub target: AgentId,
840    pub task: String,
841    pub context: Option<serde_json::Value>,
842    pub result_contract: ResultContract,
843    pub max_iterations: Option<u32>,
844    pub timeout: Option<Duration>,
845    pub metadata: AttributeBag,
846}
847
848#[derive(Debug, Clone)]
849pub struct AgentRunContextView {
850    pub run_id: RunId,
851    pub root_run_id: RunId,
852    pub parent_run_id: Option<RunId>,
853    pub agent_id: AgentId,
854    pub session_id: String,
855    pub depth: usize,
856    pub ancestry: Vec<AgentId>,
857    pub runtime_context: Option<RuntimeContext>,
858}
859
860impl AgentCallRequest {
861    pub fn new(target: impl Into<String>, task: impl Into<String>) -> Self {
862        Self {
863            target: AgentId::unchecked(target),
864            task: task.into(),
865            context: None,
866            result_contract: ResultContract::Auto,
867            max_iterations: None,
868            timeout: None,
869            metadata: AttributeBag::new(),
870        }
871    }
872
873    pub fn context(mut self, context: serde_json::Value) -> Self {
874        self.context = Some(context);
875        self
876    }
877
878    pub fn result_contract(mut self, result_contract: ResultContract) -> Self {
879        self.result_contract = result_contract;
880        self
881    }
882
883    pub fn max_iterations(mut self, max_iterations: u32) -> Self {
884        self.max_iterations = Some(max_iterations);
885        self
886    }
887
888    pub fn timeout(mut self, timeout: Duration) -> Self {
889        self.timeout = Some(timeout);
890        self
891    }
892
893    pub fn metadata(mut self, metadata: AttributeBag) -> Self {
894        self.metadata = metadata;
895        self
896    }
897}
898
899#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
900pub struct RunUsage {
901    pub input_tokens: Option<u64>,
902    pub output_tokens: Option<u64>,
903    pub total_tokens: Option<u64>,
904    pub model_calls: u32,
905    pub tool_calls: u32,
906    pub agent_calls: u32,
907    pub duration_millis: u64,
908}
909
910impl RunUsage {
911    pub fn add_assign(&mut self, other: &RunUsage) {
912        self.input_tokens = add_options(self.input_tokens, other.input_tokens);
913        self.output_tokens = add_options(self.output_tokens, other.output_tokens);
914        self.total_tokens = add_options(self.total_tokens, other.total_tokens);
915        self.model_calls = self.model_calls.saturating_add(other.model_calls);
916        self.tool_calls = self.tool_calls.saturating_add(other.tool_calls);
917        self.agent_calls = self.agent_calls.saturating_add(other.agent_calls);
918        self.duration_millis = self.duration_millis.saturating_add(other.duration_millis);
919    }
920}
921
922fn add_options(left: Option<u64>, right: Option<u64>) -> Option<u64> {
923    match (left, right) {
924        (Some(left), Some(right)) => Some(left.saturating_add(right)),
925        (Some(value), None) | (None, Some(value)) => Some(value),
926        (None, None) => None,
927    }
928}
929
930#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
931#[serde(tag = "type", content = "value", rename_all = "snake_case")]
932pub enum AgentOutput {
933    Text(String),
934    Json(serde_json::Value),
935}
936
937impl AgentOutput {
938    pub fn byte_len(&self) -> usize {
939        match self {
940            Self::Text(value) => value.len(),
941            Self::Json(value) => serde_json::to_vec(value)
942                .map(|value| value.len())
943                .unwrap_or(0),
944        }
945    }
946}
947
948#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
949pub struct AgentFailure {
950    pub code: String,
951    pub message: String,
952    pub retryable: bool,
953}
954
955#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
956#[serde(tag = "type", content = "data", rename_all = "snake_case")]
957pub enum RunOutcome {
958    Completed,
959    Failed(AgentFailure),
960    Cancelled,
961    TimedOut,
962    BudgetExceeded,
963    HandedOff { successor_run_id: RunId },
964}
965
966impl RunOutcome {
967    pub fn kind(&self) -> RunOutcomeKind {
968        match self {
969            Self::Completed => RunOutcomeKind::Completed,
970            Self::Failed(_) => RunOutcomeKind::Failed,
971            Self::Cancelled => RunOutcomeKind::Cancelled,
972            Self::TimedOut => RunOutcomeKind::TimedOut,
973            Self::BudgetExceeded => RunOutcomeKind::BudgetExceeded,
974            Self::HandedOff { .. } => RunOutcomeKind::HandedOff,
975        }
976    }
977}
978
979#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
980#[serde(rename_all = "snake_case")]
981pub enum RunOutcomeKind {
982    Completed,
983    Failed,
984    Cancelled,
985    TimedOut,
986    BudgetExceeded,
987    HandedOff,
988}
989
990#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
991pub struct ArtifactRef {
992    pub artifact_id: String,
993    pub media_type: Option<String>,
994    pub name: Option<String>,
995    pub size_bytes: Option<u64>,
996}
997
998#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
999pub struct AgentRunResult {
1000    pub run_id: RunId,
1001    pub root_run_id: RunId,
1002    pub agent_id: AgentId,
1003    pub session_id: String,
1004    pub outcome: RunOutcome,
1005    pub output: Option<AgentOutput>,
1006    pub usage: RunUsage,
1007    pub artifacts: Vec<ArtifactRef>,
1008    pub metadata: AttributeBag,
1009}
1010
1011#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1012pub struct AgentCallResult {
1013    pub schema_version: u16,
1014    pub call_id: AgentCallId,
1015    pub target: AgentId,
1016    pub run_id: Option<RunId>,
1017    pub outcome: RunOutcome,
1018    pub output: Option<AgentOutput>,
1019    pub usage: RunUsage,
1020    pub artifacts: Vec<ArtifactRef>,
1021}
1022
1023#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1024#[serde(rename_all = "snake_case")]
1025pub enum RunStatus {
1026    Created,
1027    Queued,
1028    Running,
1029    WaitingForChild,
1030    Completed,
1031    Failed,
1032    Cancelled,
1033    TimedOut,
1034    BudgetExceeded,
1035    HandedOff,
1036}
1037
1038#[derive(Debug, Clone, Serialize, Deserialize)]
1039pub struct RunRecord {
1040    pub run_id: RunId,
1041    pub root_run_id: RunId,
1042    pub parent_run_id: Option<RunId>,
1043    pub agent_call_id: Option<AgentCallId>,
1044    pub agent_id: AgentId,
1045    pub agent_definition_version: u64,
1046    pub session_id: String,
1047    pub status: RunStatus,
1048    pub depth: usize,
1049    pub started_at: String,
1050    pub finished_at: Option<String>,
1051    pub usage: RunUsage,
1052    pub failure: Option<AgentFailure>,
1053    pub metadata: AttributeBag,
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize)]
1057pub struct AgentEventEnvelope {
1058    pub schema_version: u16,
1059    pub event_id: String,
1060    pub sequence: u64,
1061    pub timestamp: String,
1062    pub root_run_id: RunId,
1063    pub run_id: RunId,
1064    pub parent_run_id: Option<RunId>,
1065    pub agent_id: AgentId,
1066    pub depth: usize,
1067    pub event: AgentEvent,
1068}
1069
1070#[derive(Debug, Clone, Serialize, Deserialize)]
1071#[serde(tag = "type", content = "data", rename_all = "snake_case")]
1072pub enum AgentEvent {
1073    RunStarted,
1074    ModelStarted,
1075    ModelDelta {
1076        content: String,
1077    },
1078    ThinkingDelta {
1079        content: String,
1080    },
1081    ModelCompleted {
1082        usage: RunUsage,
1083    },
1084    ToolStarted {
1085        tool_call_id: String,
1086        tool_name: String,
1087    },
1088    ToolCompleted {
1089        tool_call_id: String,
1090        tool_name: String,
1091        error: Option<String>,
1092    },
1093    AgentCallStarted {
1094        call_id: AgentCallId,
1095        target: AgentId,
1096    },
1097    AgentCallCompleted {
1098        call_id: AgentCallId,
1099        target: AgentId,
1100        outcome: RunOutcomeKind,
1101    },
1102    UserMessageQueued {
1103        count: usize,
1104    },
1105    RunCompleted,
1106    RunFailed {
1107        failure: AgentFailure,
1108    },
1109    RunCancelled,
1110}
1111
1112impl AgentEvent {
1113    pub fn is_lifecycle(&self) -> bool {
1114        matches!(
1115            self,
1116            Self::RunStarted
1117                | Self::AgentCallStarted { .. }
1118                | Self::AgentCallCompleted { .. }
1119                | Self::RunCompleted
1120                | Self::RunFailed { .. }
1121                | Self::RunCancelled
1122        )
1123    }
1124}
1125
1126#[derive(Debug, Clone)]
1127pub enum AgentRunCommand {
1128    EnqueueUserMessages(Vec<String>),
1129    Cancel,
1130}
1131
1132#[derive(Debug, Clone)]
1133pub(crate) struct EffectiveLimits {
1134    pub runtime: RuntimeLimits,
1135}
1136
1137impl EffectiveLimits {
1138    pub fn from_request(defaults: &RuntimeLimits, overrides: Option<&RunLimitOverrides>) -> Self {
1139        let mut runtime = defaults.clone();
1140        if let Some(overrides) = overrides {
1141            if let Some(value) = overrides.max_depth {
1142                runtime.max_depth = runtime.max_depth.min(value);
1143            }
1144            if let Some(value) = overrides.max_total_runs {
1145                runtime.max_total_runs = runtime.max_total_runs.min(value);
1146            }
1147            if let Some(value) = overrides.max_iterations_per_run {
1148                runtime.max_iterations_per_run = runtime.max_iterations_per_run.min(value);
1149            }
1150            if let Some(value) = overrides.max_result_bytes {
1151                runtime.max_result_bytes = runtime.max_result_bytes.min(value);
1152            }
1153            if let Some(value) = overrides.root_timeout {
1154                runtime.root_timeout = runtime.root_timeout.min(value);
1155            }
1156            if let Some(value) = overrides.token_budget {
1157                runtime.token_budget = Some(
1158                    runtime
1159                        .token_budget
1160                        .map(|current| current.min(value))
1161                        .unwrap_or(value),
1162                );
1163            }
1164        }
1165        Self { runtime }
1166    }
1167}
1168
1169pub(crate) fn metadata_with_lineage(
1170    base: &AttributeBag,
1171    root_run_id: &RunId,
1172    parent_run_id: Option<&RunId>,
1173    agent_call_id: Option<&AgentCallId>,
1174    agent_id: &AgentId,
1175    depth: usize,
1176) -> AttributeBag {
1177    let mut metadata = base.clone();
1178    metadata.insert("root_run_id".to_string(), serde_json::json!(root_run_id));
1179    if let Some(parent_run_id) = parent_run_id {
1180        metadata.insert(
1181            "parent_run_id".to_string(),
1182            serde_json::json!(parent_run_id),
1183        );
1184    }
1185    if let Some(agent_call_id) = agent_call_id {
1186        metadata.insert(
1187            "agent_call_id".to_string(),
1188            serde_json::json!(agent_call_id),
1189        );
1190    }
1191    metadata.insert("agent_id".to_string(), serde_json::json!(agent_id));
1192    metadata.insert("depth".to_string(), serde_json::json!(depth));
1193    if depth > 0 {
1194        metadata.insert(
1195            "session_kind".to_string(),
1196            serde_json::json!("agent_internal"),
1197        );
1198    }
1199    metadata
1200}
1201
1202pub(crate) fn merge_other(
1203    base: Option<&serde_json::Value>,
1204    overrides: Option<&serde_json::Value>,
1205) -> Option<serde_json::Value> {
1206    match (base, overrides) {
1207        (None, None) => None,
1208        (Some(value), None) | (None, Some(value)) => Some(value.clone()),
1209        (Some(serde_json::Value::Object(base)), Some(serde_json::Value::Object(overrides))) => {
1210            let mut merged = base.clone();
1211            for (key, value) in overrides {
1212                merged.insert(key.clone(), value.clone());
1213            }
1214            Some(serde_json::Value::Object(merged))
1215        }
1216        (_, Some(value)) => Some(value.clone()),
1217    }
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222    use super::*;
1223
1224    #[test]
1225    fn validates_agent_ids() {
1226        assert!(AgentId::new("researcher").is_ok());
1227        assert!(AgentId::new("code-reviewer_2").is_ok());
1228        assert!(AgentId::new("Researcher").is_err());
1229        assert!(AgentId::new("2researcher").is_err());
1230    }
1231
1232    #[test]
1233    fn request_limits_only_tighten_defaults() {
1234        let defaults = RuntimeLimits::default();
1235        let effective = EffectiveLimits::from_request(
1236            &defaults,
1237            Some(&RunLimitOverrides {
1238                max_depth: Some(defaults.max_depth + 10),
1239                max_total_runs: Some(2),
1240                ..Default::default()
1241            }),
1242        );
1243        assert_eq!(effective.runtime.max_depth, defaults.max_depth);
1244        assert_eq!(effective.runtime.max_total_runs, 2);
1245    }
1246
1247    #[test]
1248    fn model_profile_debug_redacts_api_key() {
1249        let profile = ModelProfile::builder("default", ProviderType::OpenAI, "model")
1250            .api_key("secret")
1251            .base_url("http://localhost")
1252            .build()
1253            .unwrap();
1254        let debug = format!("{profile:?}");
1255        assert!(!debug.contains("secret"));
1256        assert!(debug.contains("REDACTED"));
1257    }
1258}