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