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}