Skip to main content

codewhale_protocol/
lib.rs

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