use serde::{Deserialize, Serialize}; use crate::gateway::{ApprovalOutcome, ApprovalPosted, ApprovalReply, ApprovalRequest}; use crate::memory::{ MemoryAdmin, MemoryAdminReply, MemoryBlockReply, MemoryCall, Reindexed, SessionState, SessionStateReply, SessionUpdated, }; use crate::{CallId, DataClass, DecisionRecord, SessionId, Timestamp, Usage}; pub const PROTOCOL_VERSION: u32 = 1; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Envelope { pub v: u32, pub id: u64, pub r#final: bool, pub msg: Message, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde( tag = "kind", content = "body", deny_unknown_fields, rename_all = "snake_case" )] pub enum Message { ToolRequest(ToolRequest), ToolResponse(ToolResponse), Error(WireError), Turn(Turn), TurnEvent(TurnEvent), TurnDone(TurnDone), Approvals(Empty), ApprovalList(ApprovalList), Approve(Approve), ApproveResult(ApproveResult), Refuse(Refuse), Ok(Empty), CheckGrants(Empty), GrantsReport(GrantsReport), ApprovalRequest(ApprovalRequest), ApprovalPosted(ApprovalPosted), ApprovalReply(ApprovalReply), ApprovalOutcome(ApprovalOutcome), MemoryBlock(Empty), MemoryBlockReply(MemoryBlockReply), SessionUpdated(SessionUpdated), MemoryCall(MemoryCall), Reindex(Empty), Reindexed(Reindexed), MemoryAdmin(MemoryAdmin), MemoryAdminReply(MemoryAdminReply), SessionState(SessionState), SessionStateReply(SessionStateReply), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct WireError { pub code: ErrorCode, pub detail: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ErrorCode { BadFrame, BadVersion, BadMessage, Internal, SessionFull, TurnLimit, SessionBusy, NoSuchSession, SessionExists, Inference, Forbidden, NoSuchApproval, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ToolRequest { pub session: SessionId, pub call: CallId, pub tool: String, pub arguments: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "status", deny_unknown_fields, rename_all = "snake_case")] pub enum ToolResponse { PendingApproval { approval: u64, expires: Timestamp, }, Result { content: String, class: DataClass, untrusted: bool, truncated: bool, }, Failed { message: String, }, Denied { reason: DenyReason, /// Why the arguments were not valid, for `invalid_arguments` only. Written only when /// present, so a denial without one has the same bytes as before M5b. #[serde(default, skip_serializing_if = "Option::is_none")] detail: Option, }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DenyReason { NoGrant, GrantExpired, TaintTooHigh, DeniedByGrant, ApprovalRefused, ApprovalExpired, GrantsInvalid, AuditUnavailable, InvalidArguments, StateUnreadable, } // JSON: {"kind":"turn","body":{"session":"…","content":"…","resume":false,"speaker":"kyle"}} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Turn { pub session: SessionId, pub content: String, pub resume: bool, /// Who spoke the turn, when one person did (M5c spec, section 11). Written only when present, /// and a frame without it reads as `None`, the safe side: a turn without a speaker is never /// synced to Honcho. #[serde(default, skip_serializing_if = "Option::is_none")] pub speaker: Option, } // JSON: {"event":"content","text":"…"} — the tag sits beside the fields, snake_case #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "event", deny_unknown_fields, rename_all = "snake_case")] pub enum TurnEvent { Queued { ahead: u64, }, Waiting { slot_busy: bool, }, Progress { total: u64, cache: u64, processed: u64, }, Reasoning { text: String, }, Content { text: String, }, ToolCallStarted { name: String, }, ToolResult { name: String, class: DataClass, truncated: bool, }, ThinkingCapped { tokens: u64, }, Retrying { attempt: u32, after_ms: u64, error: String, }, CacheLoss { expected: u64, got: u64, }, ApprovalPending { approval: u64, tool: String, expires: Timestamp, }, ToolDenied { name: String, reason: DenyReason, }, } // JSON: {"kind":"turn_done","body":{"content":"…","usage":{…},"untrusted":false}} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TurnDone { pub content: String, pub usage: Usage, /// True once the session has read untrusted content, in this turn or an earlier one (M5b /// spec, section 9). It has no default: a frame without it is refused, never read as clean. pub untrusted: bool, } // The admin messages on admin.sock. JSON: {"kind":"approvals","body":{}} #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Empty {} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PendingApproval { pub approval: u64, pub session: SessionId, pub call: CallId, pub tool: String, pub arguments: String, pub grant: String, pub taint: DataClass, pub created: Timestamp, pub expires: Timestamp, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ApprovalList { pub items: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Approve { pub approval: u64, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ApproveResult { pub outcome: DecisionRecord, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Refuse { pub approval: u64, pub reason: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct GrantProblem { pub file: String, pub line: Option, pub problem: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct GrantsReport { pub problems: Vec, }