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    /// Optional system prompt to prepend to the conversation.
33    /// Used for skill catalogs, persona blocks, and context compiler output.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub system_prompt: Option<String>,
36    /// Tool whitelist from active skill. When set, only these tools are sent to the LLM.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub allowed_tools: Option<Vec<String>>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(tag = "type", rename_all = "snake_case")]
43pub enum ModelDirective {
44    TextDelta {
45        delta: String,
46        #[serde(default, skip_serializing_if = "Option::is_none")]
47        index: Option<u32>,
48    },
49    Message {
50        role: String,
51        content: String,
52    },
53    ToolCall {
54        call: ToolCall,
55    },
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum ModelStopReason {
61    Completed,
62    ToolCall,
63    MaxIterations,
64    Cancelled,
65    Error,
66    Other(String),
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ModelCompletion {
71    pub provider: String,
72    pub model: String,
73    #[serde(default)]
74    pub directives: Vec<ModelDirective>,
75    pub stop_reason: ModelStopReason,
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub usage: Option<TokenUsage>,
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub final_answer: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ToolExecutionRequest {
84    pub session_id: SessionId,
85    pub workspace_root: String,
86    pub call: ToolCall,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ToolExecutionReport {
91    pub tool_run_id: ToolRunId,
92    pub call_id: String,
93    pub tool_name: String,
94    pub exit_status: i32,
95    pub duration_ms: u64,
96    pub outcome: ToolOutcome,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct PolicyGateDecision {
101    #[serde(default)]
102    pub allowed: Vec<Capability>,
103    #[serde(default)]
104    pub requires_approval: Vec<Capability>,
105    #[serde(default)]
106    pub denied: Vec<Capability>,
107}
108
109impl PolicyGateDecision {
110    pub fn is_allowed_now(&self) -> bool {
111        self.denied.is_empty() && self.requires_approval.is_empty()
112    }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ApprovalRequest {
117    pub session_id: SessionId,
118    pub call_id: String,
119    pub tool_name: String,
120    pub capability: Capability,
121    pub reason: String,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ApprovalTicket {
126    pub approval_id: ApprovalId,
127    pub session_id: SessionId,
128    pub call_id: String,
129    pub tool_name: String,
130    pub capability: Capability,
131    pub reason: String,
132    pub created_at: DateTime<Utc>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ApprovalResolution {
137    pub approval_id: ApprovalId,
138    pub approved: bool,
139    pub actor: String,
140    pub resolved_at: DateTime<Utc>,
141}
142
143#[async_trait]
144pub trait EventStorePort: Send + Sync {
145    async fn append(&self, event: EventRecord) -> KernelResult<EventRecord>;
146    async fn read(
147        &self,
148        session_id: SessionId,
149        branch_id: BranchId,
150        from_sequence: u64,
151        limit: usize,
152    ) -> KernelResult<Vec<EventRecord>>;
153    async fn head(&self, session_id: SessionId, branch_id: BranchId) -> KernelResult<u64>;
154    async fn subscribe(
155        &self,
156        session_id: SessionId,
157        branch_id: BranchId,
158        after_sequence: u64,
159    ) -> KernelResult<EventRecordStream>;
160}
161
162#[async_trait]
163pub trait ModelProviderPort: Send + Sync {
164    async fn complete(&self, request: ModelCompletionRequest) -> KernelResult<ModelCompletion>;
165}
166
167#[async_trait]
168pub trait ToolHarnessPort: Send + Sync {
169    async fn execute(&self, request: ToolExecutionRequest) -> KernelResult<ToolExecutionReport>;
170}
171
172#[async_trait]
173pub trait PolicyGatePort: Send + Sync {
174    async fn evaluate(
175        &self,
176        session_id: SessionId,
177        requested: Vec<Capability>,
178    ) -> KernelResult<PolicyGateDecision>;
179
180    async fn set_policy(
181        &self,
182        _session_id: SessionId,
183        _policy: crate::policy::PolicySet,
184    ) -> KernelResult<()> {
185        Ok(())
186    }
187}
188
189#[async_trait]
190pub trait ApprovalPort: Send + Sync {
191    async fn enqueue(&self, request: ApprovalRequest) -> KernelResult<ApprovalTicket>;
192    async fn list_pending(&self, session_id: SessionId) -> KernelResult<Vec<ApprovalTicket>>;
193    async fn resolve(
194        &self,
195        approval_id: ApprovalId,
196        approved: bool,
197        actor: String,
198    ) -> KernelResult<ApprovalResolution>;
199}