Skip to main content

aios_protocol/
ports.rs

1//! Canonical runtime ports for Agent OS integrations.
2//!
3//! These traits define the only allowed runtime boundary between the kernel
4//! engine and external implementations (event stores, model providers, tool
5//! harnesses, policy engines, approval systems, and memory backends).
6//!
7//! Object-safety note:
8//! - Traits use `async-trait` for async dyn-dispatch.
9//! - Streaming uses boxed trait objects (`EventRecordStream`).
10
11use crate::error::KernelResult;
12use crate::event::{EventRecord, TokenUsage};
13use crate::ids::{ApprovalId, BranchId, RunId, SessionId, ToolRunId};
14use crate::policy::Capability;
15use crate::tool::{ToolCall, ToolOutcome};
16use async_trait::async_trait;
17use chrono::{DateTime, Utc};
18use futures_util::stream::BoxStream;
19use serde::{Deserialize, Serialize};
20
21pub type EventRecordStream = BoxStream<'static, KernelResult<EventRecord>>;
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ModelCompletionRequest {
25    pub session_id: SessionId,
26    pub branch_id: BranchId,
27    pub run_id: RunId,
28    pub step_index: u32,
29    pub objective: String,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub proposed_tool: Option<ToolCall>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(tag = "type", rename_all = "snake_case")]
36pub enum ModelDirective {
37    TextDelta {
38        delta: String,
39        #[serde(default, skip_serializing_if = "Option::is_none")]
40        index: Option<u32>,
41    },
42    Message {
43        role: String,
44        content: String,
45    },
46    ToolCall {
47        call: ToolCall,
48    },
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum ModelStopReason {
54    Completed,
55    ToolCall,
56    MaxIterations,
57    Cancelled,
58    Error,
59    Other(String),
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ModelCompletion {
64    pub provider: String,
65    pub model: String,
66    #[serde(default)]
67    pub directives: Vec<ModelDirective>,
68    pub stop_reason: ModelStopReason,
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub usage: Option<TokenUsage>,
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub final_answer: Option<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ToolExecutionRequest {
77    pub session_id: SessionId,
78    pub workspace_root: String,
79    pub call: ToolCall,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ToolExecutionReport {
84    pub tool_run_id: ToolRunId,
85    pub call_id: String,
86    pub tool_name: String,
87    pub exit_status: i32,
88    pub duration_ms: u64,
89    pub outcome: ToolOutcome,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct PolicyGateDecision {
94    #[serde(default)]
95    pub allowed: Vec<Capability>,
96    #[serde(default)]
97    pub requires_approval: Vec<Capability>,
98    #[serde(default)]
99    pub denied: Vec<Capability>,
100}
101
102impl PolicyGateDecision {
103    pub fn is_allowed_now(&self) -> bool {
104        self.denied.is_empty() && self.requires_approval.is_empty()
105    }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ApprovalRequest {
110    pub session_id: SessionId,
111    pub call_id: String,
112    pub tool_name: String,
113    pub capability: Capability,
114    pub reason: String,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct ApprovalTicket {
119    pub approval_id: ApprovalId,
120    pub session_id: SessionId,
121    pub call_id: String,
122    pub tool_name: String,
123    pub capability: Capability,
124    pub reason: String,
125    pub created_at: DateTime<Utc>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ApprovalResolution {
130    pub approval_id: ApprovalId,
131    pub approved: bool,
132    pub actor: String,
133    pub resolved_at: DateTime<Utc>,
134}
135
136#[async_trait]
137pub trait EventStorePort: Send + Sync {
138    async fn append(&self, event: EventRecord) -> KernelResult<EventRecord>;
139    async fn read(
140        &self,
141        session_id: SessionId,
142        branch_id: BranchId,
143        from_sequence: u64,
144        limit: usize,
145    ) -> KernelResult<Vec<EventRecord>>;
146    async fn head(&self, session_id: SessionId, branch_id: BranchId) -> KernelResult<u64>;
147    async fn subscribe(
148        &self,
149        session_id: SessionId,
150        branch_id: BranchId,
151        after_sequence: u64,
152    ) -> KernelResult<EventRecordStream>;
153}
154
155#[async_trait]
156pub trait ModelProviderPort: Send + Sync {
157    async fn complete(&self, request: ModelCompletionRequest) -> KernelResult<ModelCompletion>;
158}
159
160#[async_trait]
161pub trait ToolHarnessPort: Send + Sync {
162    async fn execute(&self, request: ToolExecutionRequest) -> KernelResult<ToolExecutionReport>;
163}
164
165#[async_trait]
166pub trait PolicyGatePort: Send + Sync {
167    async fn evaluate(
168        &self,
169        session_id: SessionId,
170        requested: Vec<Capability>,
171    ) -> KernelResult<PolicyGateDecision>;
172
173    async fn set_policy(
174        &self,
175        _session_id: SessionId,
176        _policy: crate::policy::PolicySet,
177    ) -> KernelResult<()> {
178        Ok(())
179    }
180}
181
182#[async_trait]
183pub trait ApprovalPort: Send + Sync {
184    async fn enqueue(&self, request: ApprovalRequest) -> KernelResult<ApprovalTicket>;
185    async fn list_pending(&self, session_id: SessionId) -> KernelResult<Vec<ApprovalTicket>>;
186    async fn resolve(
187        &self,
188        approval_id: ApprovalId,
189        approved: bool,
190        actor: String,
191    ) -> KernelResult<ApprovalResolution>;
192}