Skip to main content

codewhale_protocol/
lib.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7pub mod runtime {
8    use super::*;
9
10    pub const RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION: u32 = 1;
11
12    #[derive(Debug, Clone, Serialize, Deserialize)]
13    pub struct RuntimeEventEnvelope {
14        #[serde(default = "default_runtime_event_envelope_schema_version")]
15        pub schema_version: u32,
16        pub seq: u64,
17        pub event: String,
18        pub kind: String,
19        pub thread_id: String,
20        pub turn_id: Option<String>,
21        pub item_id: Option<String>,
22        pub timestamp: String,
23        #[serde(skip_serializing_if = "Option::is_none")]
24        pub created_at: Option<String>,
25        pub payload: Value,
26        #[serde(default)]
27        #[serde(flatten)]
28        pub extra: BTreeMap<String, Value>,
29    }
30
31    fn default_runtime_event_envelope_schema_version() -> u32 {
32        RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION
33    }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Envelope<T> {
38    pub request_id: String,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub thread_id: Option<String>,
41    pub body: T,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45#[serde(rename_all = "snake_case")]
46pub enum ThreadStatus {
47    Running,
48    Idle,
49    Completed,
50    Failed,
51    Paused,
52    Archived,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "snake_case")]
57pub enum SessionSource {
58    Interactive,
59    Resume,
60    Fork,
61    Api,
62    Unknown,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct Thread {
67    pub id: String,
68    pub preview: String,
69    pub ephemeral: bool,
70    pub model_provider: String,
71    pub created_at: i64,
72    pub updated_at: i64,
73    pub status: ThreadStatus,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub path: Option<PathBuf>,
76    pub cwd: PathBuf,
77    pub cli_version: String,
78    pub source: SessionSource,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub name: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ThreadStartParams {
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub model: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub model_provider: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub cwd: Option<PathBuf>,
91    #[serde(default)]
92    pub persist_extended_history: bool,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ThreadResumeParams {
97    pub thread_id: String,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub history: Option<Vec<Value>>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub path: Option<PathBuf>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub model: Option<String>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub model_provider: Option<String>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub cwd: Option<PathBuf>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub approval_policy: Option<String>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub sandbox: Option<String>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub config: Option<Value>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub base_instructions: Option<String>,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub developer_instructions: Option<String>,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub personality: Option<String>,
120    #[serde(default)]
121    pub persist_extended_history: bool,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ThreadForkParams {
126    pub thread_id: String,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub path: Option<PathBuf>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub model: Option<String>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub model_provider: Option<String>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub cwd: Option<PathBuf>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub approval_policy: Option<String>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub sandbox: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub config: Option<Value>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub base_instructions: Option<String>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub developer_instructions: Option<String>,
145    #[serde(default)]
146    pub persist_extended_history: bool,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ThreadListParams {
151    #[serde(default)]
152    pub include_archived: bool,
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub limit: Option<usize>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ThreadReadParams {
159    pub thread_id: String,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct ThreadSetNameParams {
164    pub thread_id: String,
165    pub name: String,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(tag = "kind", rename_all = "snake_case")]
170pub enum ThreadRequest {
171    Create {
172        #[serde(default)]
173        metadata: Value,
174    },
175    Start(ThreadStartParams),
176    Resume(ThreadResumeParams),
177    Fork(ThreadForkParams),
178    List(ThreadListParams),
179    Read(ThreadReadParams),
180    SetName(ThreadSetNameParams),
181    Archive {
182        thread_id: String,
183    },
184    Unarchive {
185        thread_id: String,
186    },
187    Message {
188        thread_id: String,
189        input: String,
190    },
191}
192
193/// Response to a [`ThreadRequest`].
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct ThreadResponse {
196    /// The thread this response pertains to.
197    pub thread_id: String,
198    /// Human-readable status string (e.g. `"ok"`, `"error"`).
199    pub status: String,
200    /// The thread details, when a single thread is returned.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub thread: Option<Thread>,
203    /// List of threads, populated by `List` requests.
204    #[serde(default)]
205    pub threads: Vec<Thread>,
206    /// The model used for the thread, if applicable.
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub model: Option<String>,
209    /// The model provider used for the thread.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub model_provider: Option<String>,
212    /// The working directory of the thread.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub cwd: Option<PathBuf>,
215    /// The active approval policy.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub approval_policy: Option<String>,
218    /// The active sandbox configuration.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub sandbox: Option<String>,
221    /// Streaming events associated with this response.
222    #[serde(default)]
223    pub events: Vec<EventFrame>,
224    /// Arbitrary additional response data.
225    #[serde(default)]
226    pub data: Value,
227}
228
229/// Application-level requests that are not tied to a specific thread.
230#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(tag = "kind", rename_all = "snake_case")]
232pub enum AppRequest {
233    /// Query the server's capabilities.
234    Capabilities,
235    /// Read a configuration value by key.
236    ConfigGet { key: String },
237    /// Set a configuration key to a value.
238    ConfigSet { key: String, value: String },
239    /// Remove a configuration key.
240    ConfigUnset { key: String },
241    /// List all configuration entries.
242    ConfigList,
243    /// List available models.
244    Models,
245    /// List threads that are currently loaded in memory.
246    ThreadLoadedList,
247}
248
249/// Response to an [`AppRequest`].
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct AppResponse {
252    /// Whether the request succeeded.
253    pub ok: bool,
254    /// The response payload.
255    pub data: Value,
256    /// Streaming events associated with this response.
257    #[serde(default)]
258    pub events: Vec<EventFrame>,
259}
260
261/// A simple prompt request that sends text to the model and returns output.
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct PromptRequest {
264    /// Optional thread context for the prompt.
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub thread_id: Option<String>,
267    /// The prompt text.
268    pub prompt: String,
269    /// Model override, or the default if omitted.
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub model: Option<String>,
272}
273
274/// Response to a [`PromptRequest`].
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct PromptResponse {
277    /// The model's output text.
278    pub output: String,
279    /// The model that produced the output.
280    pub model: String,
281    /// Streaming events associated with this response.
282    #[serde(default)]
283    pub events: Vec<EventFrame>,
284}
285
286/// Policy controlling when the agent must ask the user for approval before acting.
287#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
288#[serde(rename_all = "snake_case")]
289pub enum AskForApproval {
290    /// Ask for approval unless the action is on a trusted path/resource.
291    UnlessTrusted,
292    /// Only ask after a tool call fails.
293    OnFailure,
294    /// Ask every time a tool call is requested.
295    OnRequest,
296    /// Reject the action without asking, with details on which categories are blocked.
297    Reject {
298        sandbox_approval: bool,
299        rules: bool,
300        mcp_elicitations: bool,
301    },
302    /// Never ask; auto-approve all actions.
303    Never,
304}
305
306/// Classification of tool invocation origin.
307#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
308#[serde(rename_all = "snake_case")]
309pub enum ToolKind {
310    /// A built-in function tool.
311    Function,
312    /// An MCP (Model Context Protocol) tool.
313    Mcp,
314}
315
316/// Parameters for executing a local shell command.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct LocalShellParams {
319    /// The shell command to execute.
320    pub command: String,
321    /// Working directory for the command.
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub cwd: Option<String>,
324    /// Timeout in milliseconds.
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub timeout_ms: Option<u64>,
327}
328
329/// The payload of a tool call, discriminated by tool type.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331#[serde(tag = "type", rename_all = "snake_case")]
332pub enum ToolPayload {
333    /// A built-in function call with JSON-encoded arguments.
334    Function { arguments: String },
335    /// A custom tool invocation with a free-form input string.
336    Custom { input: String },
337    /// A local shell command execution.
338    LocalShell { params: LocalShellParams },
339    /// An MCP tool invocation targeting a specific server and tool.
340    Mcp {
341        server: String,
342        tool: String,
343        raw_arguments: Value,
344        #[serde(skip_serializing_if = "Option::is_none")]
345        raw_tool_call_id: Option<String>,
346    },
347}
348
349/// The result of a tool call, discriminated by tool type.
350#[derive(Debug, Clone, Serialize, Deserialize)]
351#[serde(tag = "type", rename_all = "snake_case")]
352pub enum ToolOutput {
353    /// Result of a built-in function call.
354    Function {
355        /// The output body, if any.
356        #[serde(skip_serializing_if = "Option::is_none")]
357        body: Option<Value>,
358        /// Whether the call succeeded.
359        success: bool,
360    },
361    /// Result of an MCP tool call.
362    Mcp {
363        /// The result value returned by the MCP server.
364        result: Value,
365    },
366}
367
368/// Action to take for a network policy rule.
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
370#[serde(rename_all = "snake_case")]
371pub enum NetworkPolicyRuleAction {
372    /// Allow network access to the host.
373    Allow,
374    /// Deny network access to the host.
375    Deny,
376}
377
378/// A proposed amendment to the network access policy for a specific host.
379#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
380pub struct NetworkPolicyAmendment {
381    /// The host to amend the policy for.
382    pub host: String,
383    /// The action to apply.
384    pub action: NetworkPolicyRuleAction,
385}
386
387/// A user's decision on an approval request.
388#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
389#[serde(tag = "type", rename_all = "snake_case")]
390pub enum ReviewDecision {
391    /// Approve the action.
392    Approved,
393    /// Approve and also amend the execution policy.
394    ApprovedExecpolicyAmendment,
395    /// Approve for the remainder of this session only.
396    ApprovedForSession,
397    /// Approve with a network policy amendment.
398    NetworkPolicyAmendment {
399        host: String,
400        action: NetworkPolicyRuleAction,
401    },
402    /// Deny the action.
403    Denied,
404    /// Abort the entire turn.
405    Abort,
406}
407
408/// Status of an MCP server during startup.
409#[derive(Debug, Clone, Serialize, Deserialize)]
410#[serde(rename_all = "snake_case")]
411pub enum McpStartupStatus {
412    /// The server is in the process of starting.
413    Starting,
414    /// The server is ready to accept requests.
415    Ready,
416    /// The server failed to start.
417    Failed { error: String },
418    /// Startup was cancelled.
419    Cancelled,
420}
421
422/// A progress update for a single MCP server's startup.
423#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct McpStartupUpdateEvent {
425    /// Name of the MCP server.
426    pub server_name: String,
427    /// Current startup status.
428    pub status: McpStartupStatus,
429}
430
431/// Details of an MCP server that failed to start.
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct McpStartupFailure {
434    /// Name of the MCP server that failed.
435    pub server_name: String,
436    /// Error description.
437    pub error: String,
438}
439
440/// Summary event emitted once all MCP servers have finished starting.
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct McpStartupCompleteEvent {
443    /// Servers that started successfully.
444    pub ready: Vec<String>,
445    /// Servers that failed to start.
446    pub failed: Vec<McpStartupFailure>,
447    /// Servers whose startup was cancelled.
448    pub cancelled: Vec<String>,
449}
450
451/// Context about a network access request that requires approval.
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct NetworkApprovalContext {
454    /// The host being accessed.
455    pub host: String,
456    /// The network protocol (e.g. `"https"`, `"tcp"`).
457    pub protocol: String,
458}
459
460/// An event requesting user approval for a command execution or patch application.
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct ExecApprovalRequestEvent {
463    /// Identifier of the tool call requesting approval.
464    pub call_id: String,
465    /// Unique identifier for this approval request.
466    pub approval_id: String,
467    /// The turn during which the request was made.
468    pub turn_id: String,
469    /// The command that would be executed.
470    pub command: String,
471    /// The working directory for the command.
472    pub cwd: String,
473    /// Human-readable reason why approval is needed.
474    pub reason: String,
475    /// Network context if the approval involves network access.
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub network_approval_context: Option<NetworkApprovalContext>,
478    /// Proposed execution policy rule amendments.
479    #[serde(default)]
480    pub proposed_execpolicy_amendment: Vec<String>,
481    /// Proposed network policy amendments.
482    #[serde(default)]
483    pub proposed_network_policy_amendments: Vec<NetworkPolicyAmendment>,
484    /// Additional permissions being requested.
485    #[serde(default)]
486    pub additional_permissions: Vec<String>,
487    /// The set of decisions the user can choose from.
488    #[serde(default)]
489    pub available_decisions: Vec<ReviewDecision>,
490}
491
492/// The channel a response delta is being written to.
493#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
494#[serde(rename_all = "snake_case")]
495pub enum ResponseChannel {
496    /// The main visible text output.
497    #[default]
498    Text,
499    /// Internal reasoning / chain-of-thought output.
500    Reasoning,
501}
502
503impl ResponseChannel {
504    /// Returns `true` if this is the `Text` channel.
505    pub const fn is_text(&self) -> bool {
506        matches!(self, ResponseChannel::Text)
507    }
508}
509
510/// A user's approval decision sent in response to an approval request.
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct ApprovalDecisionRequest {
513    /// The decision identifier (e.g. `"approved"`, `"denied"`).
514    pub decision: String,
515    /// Whether to remember this decision for future similar requests.
516    #[serde(default)]
517    pub remember: bool,
518}
519
520/// A single streaming event frame emitted during agent execution.
521///
522/// Events are tagged by the `event` field and cover the full lifecycle of a
523/// turn: response streaming, tool calls, MCP lifecycle, command execution,
524/// patch application, approvals, and errors.
525#[derive(Debug, Clone, Serialize, Deserialize)]
526#[serde(tag = "event", rename_all = "snake_case")]
527pub enum EventFrame {
528    /// A new model response has started.
529    ResponseStart { response_id: String },
530    /// A incremental text delta for an in-progress response.
531    ResponseDelta {
532        response_id: String,
533        delta: String,
534        #[serde(default, skip_serializing_if = "ResponseChannel::is_text")]
535        channel: ResponseChannel,
536    },
537    /// The model response has finished.
538    ResponseEnd { response_id: String },
539    /// A tool call has begun.
540    ToolCallStart {
541        response_id: String,
542        tool_name: String,
543        arguments: Value,
544    },
545    /// A tool call has completed and produced a result.
546    ToolCallResult {
547        response_id: String,
548        tool_name: String,
549        output: Value,
550    },
551    /// Progress update for an MCP server starting up.
552    McpStartupUpdate { update: McpStartupUpdateEvent },
553    /// All MCP servers have finished starting.
554    McpStartupComplete { summary: McpStartupCompleteEvent },
555    /// An MCP tool call has begun.
556    McpToolCallBegin {
557        server_name: String,
558        tool_name: String,
559    },
560    /// An MCP tool call has finished.
561    McpToolCallEnd {
562        server_name: String,
563        tool_name: String,
564        ok: bool,
565    },
566    /// User approval is needed for a command execution.
567    ExecApprovalRequest { request: ExecApprovalRequestEvent },
568    /// User approval is needed for applying a patch.
569    ApplyPatchApprovalRequest { request: ExecApprovalRequestEvent },
570    /// An MCP server is requesting user input (elicitation).
571    ElicitationRequest {
572        server_name: String,
573        request_id: String,
574        prompt: String,
575    },
576    /// A command has started executing.
577    ExecCommandBegin { command: String, cwd: String },
578    /// Incremental output from a running command.
579    ExecCommandOutputDelta { command: String, delta: String },
580    /// A command has finished executing.
581    ExecCommandEnd { command: String, exit_code: i32 },
582    /// A patch has started being applied to a file.
583    PatchApplyBegin { path: String },
584    /// A patch has finished being applied.
585    PatchApplyEnd { path: String, ok: bool },
586    /// A new turn has started within a thread.
587    TurnStarted { turn_id: String },
588    /// A turn has completed successfully.
589    TurnComplete { turn_id: String },
590    /// A turn was aborted before completion.
591    TurnAborted { turn_id: String, reason: String },
592    /// An error occurred during processing.
593    Error {
594        response_id: String,
595        message: String,
596    },
597}