Skip to main content

codewhale_protocol/
lib.rs

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