Skip to main content

codewhale_protocol/
lib.rs

1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6pub mod agent_run;
7pub mod fleet;
8pub mod runtime;
9pub mod workroom;
10
11/// Common trait for lifecycle status enums across the protocol layer.
12///
13/// Every status enum — thread, goal, fleet run, worker, and job status —
14/// implements this trait so generic code can ask three universal questions
15/// without matching on every variant.
16pub trait Status {
17    /// Returns `true` when this status represents a final, non-progressable state
18    /// (e.g. Completed, Failed, Cancelled, Archived, Retired).
19    fn is_terminal(&self) -> bool;
20
21    /// Returns `true` when work is currently in-flight
22    /// (e.g. Running, Active, Busy, Queued, Pending).
23    fn is_active(&self) -> bool;
24
25    /// Returns `true` when the item has been explicitly paused by the user
26    /// or system (e.g. Paused).
27    fn is_paused(&self) -> bool;
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Envelope<T> {
32    pub request_id: String,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub thread_id: Option<String>,
35    pub body: T,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum ThreadStatus {
41    Running,
42    Idle,
43    Completed,
44    Failed,
45    Paused,
46    Archived,
47}
48
49impl Status for ThreadStatus {
50    fn is_terminal(&self) -> bool {
51        matches!(self, Self::Completed | Self::Failed | Self::Archived)
52    }
53    fn is_active(&self) -> bool {
54        matches!(self, Self::Running)
55    }
56    fn is_paused(&self) -> bool {
57        matches!(self, Self::Paused)
58    }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62#[serde(rename_all = "snake_case")]
63pub enum SessionSource {
64    Interactive,
65    Resume,
66    Fork,
67    Api,
68    Unknown,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Thread {
73    pub id: String,
74    pub preview: String,
75    pub ephemeral: bool,
76    pub model_provider: String,
77    pub created_at: i64,
78    pub updated_at: i64,
79    pub status: ThreadStatus,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub path: Option<PathBuf>,
82    pub cwd: PathBuf,
83    pub cli_version: String,
84    pub source: SessionSource,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub name: Option<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
90#[serde(rename_all = "snake_case")]
91pub enum ThreadGoalStatus {
92    Active,
93    Paused,
94    Blocked,
95    UsageLimited,
96    BudgetLimited,
97    Complete,
98}
99
100impl Status for ThreadGoalStatus {
101    fn is_terminal(&self) -> bool {
102        matches!(self, Self::Complete)
103    }
104    fn is_active(&self) -> bool {
105        matches!(self, Self::Active)
106    }
107    fn is_paused(&self) -> bool {
108        matches!(self, Self::Paused)
109    }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
113pub struct ThreadGoal {
114    pub thread_id: String,
115    pub goal_id: String,
116    pub objective: String,
117    pub status: ThreadGoalStatus,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub token_budget: Option<i64>,
120    pub tokens_used: i64,
121    pub time_used_seconds: i64,
122    pub continuation_count: i64,
123    pub created_at: i64,
124    pub updated_at: i64,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ThreadStartParams {
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(default)]
136    pub persist_extended_history: bool,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ThreadResumeParams {
141    pub thread_id: String,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub history: Option<Vec<Value>>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub path: Option<PathBuf>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub model: Option<String>,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub model_provider: Option<String>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub cwd: Option<PathBuf>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub approval_policy: Option<String>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub sandbox: Option<String>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub config: Option<Value>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub base_instructions: Option<String>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub developer_instructions: Option<String>,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub personality: Option<String>,
164    #[serde(default)]
165    pub persist_extended_history: bool,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct ThreadForkParams {
170    pub thread_id: String,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub path: Option<PathBuf>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub model: Option<String>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub model_provider: Option<String>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub cwd: Option<PathBuf>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub approval_policy: Option<String>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub sandbox: Option<String>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub config: Option<Value>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub base_instructions: Option<String>,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub developer_instructions: Option<String>,
189    #[serde(default)]
190    pub persist_extended_history: bool,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ThreadListParams {
195    #[serde(default)]
196    pub include_archived: bool,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub limit: Option<usize>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct ThreadReadParams {
203    pub thread_id: String,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ThreadSetNameParams {
208    pub thread_id: String,
209    pub name: String,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct ThreadGoalSetParams {
214    pub thread_id: String,
215    pub objective: String,
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub token_budget: Option<i64>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct ThreadGoalGetParams {
222    pub thread_id: String,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ThreadGoalClearParams {
227    pub thread_id: String,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct ThreadGoalProgressParams {
232    pub thread_id: String,
233    #[serde(default)]
234    pub token_delta: i64,
235    #[serde(default)]
236    pub time_delta_seconds: i64,
237    #[serde(default)]
238    pub record_continuation: bool,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
242#[serde(tag = "kind", rename_all = "snake_case")]
243pub enum ThreadRequest {
244    Create {
245        #[serde(default)]
246        metadata: Value,
247    },
248    Start(ThreadStartParams),
249    Resume(ThreadResumeParams),
250    Fork(ThreadForkParams),
251    List(ThreadListParams),
252    Read(ThreadReadParams),
253    SetName(ThreadSetNameParams),
254    GoalSet(ThreadGoalSetParams),
255    GoalGet(ThreadGoalGetParams),
256    GoalClear(ThreadGoalClearParams),
257    GoalRecordProgress(ThreadGoalProgressParams),
258    Archive {
259        thread_id: String,
260    },
261    Unarchive {
262        thread_id: String,
263    },
264    Message {
265        thread_id: String,
266        input: String,
267    },
268}
269
270/// Response to a [`ThreadRequest`].
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ThreadResponse {
273    /// The thread this response pertains to.
274    pub thread_id: String,
275    /// Human-readable status string (e.g. `"ok"`, `"error"`).
276    pub status: String,
277    /// The thread details, when a single thread is returned.
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub thread: Option<Thread>,
280    /// List of threads, populated by `List` requests.
281    #[serde(default)]
282    pub threads: Vec<Thread>,
283    /// Thread goal returned by goal get/set requests.
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub goal: Option<ThreadGoal>,
286    /// The model used for the thread, if applicable.
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub model: Option<String>,
289    /// The model provider used for the thread.
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub model_provider: Option<String>,
292    /// The working directory of the thread.
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub cwd: Option<PathBuf>,
295    /// The active approval policy.
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub approval_policy: Option<String>,
298    /// The active sandbox configuration.
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub sandbox: Option<String>,
301    /// Streaming events associated with this response.
302    #[serde(default)]
303    pub events: Vec<EventFrame>,
304    /// Arbitrary additional response data.
305    #[serde(default)]
306    pub data: Value,
307}
308
309/// Application-level requests that are not tied to a specific thread.
310#[derive(Debug, Clone, Serialize, Deserialize)]
311#[serde(tag = "kind", rename_all = "snake_case")]
312pub enum AppRequest {
313    /// Query the server's capabilities.
314    Capabilities,
315    /// Read a configuration value by key.
316    ConfigGet { key: String },
317    /// Set a configuration key to a value.
318    ConfigSet { key: String, value: String },
319    /// Remove a configuration key.
320    ConfigUnset { key: String },
321    /// List all configuration entries.
322    ConfigList,
323    /// Reload configuration from disk and apply to the live runtime.
324    ///
325    /// Re-reads both `config.toml` and the sibling `permissions.toml`,
326    /// refreshing the live `Runtime.config` and `Runtime.exec_policy`
327    /// so headless clients can pick up external config-file *and*
328    /// permission-rule edits without restarting.
329    ///
330    /// Mirrors the TUI `reload_runtime_config` codepath for everything
331    /// reachable from the headless `Runtime`. MCP server connections
332    /// are not refreshed — changing `mcp_config_path` or the referenced
333    /// `mcp.json` still requires a headless-runtime restart. The TUI's
334    /// explicit `/mcp reload` operation is not part of this protocol path.
335    ConfigReload,
336    /// List available models.
337    Models,
338    /// List threads that are currently loaded in memory.
339    ThreadLoadedList,
340    /// Submit answers to a prior [`EventFrame::UserInputRequest`].
341    ///
342    /// `request_id` must match a pending clarification request. Headless
343    /// clients use this to return the user's selections back to the runtime.
344    SubmitUserInput {
345        request_id: String,
346        answers: Vec<UserInputAnswerEvent>,
347    },
348}
349
350/// Response to an [`AppRequest`].
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct AppResponse {
353    /// Whether the request succeeded.
354    pub ok: bool,
355    /// The response payload.
356    pub data: Value,
357    /// Streaming events associated with this response.
358    #[serde(default)]
359    pub events: Vec<EventFrame>,
360}
361
362/// A simple prompt request that sends text to the model and returns output.
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct PromptRequest {
365    /// Optional thread context for the prompt.
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub thread_id: Option<String>,
368    /// The prompt text.
369    pub prompt: String,
370    /// Model override, or the default if omitted.
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub model: Option<String>,
373}
374
375/// Response to a [`PromptRequest`].
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct PromptResponse {
378    /// The model's output text.
379    pub output: String,
380    /// The model that produced the output.
381    pub model: String,
382    /// Streaming events associated with this response.
383    #[serde(default)]
384    pub events: Vec<EventFrame>,
385}
386
387/// Policy controlling when the agent must ask the user for approval before acting.
388#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
389#[serde(rename_all = "snake_case")]
390pub enum AskForApproval {
391    /// Ask for approval unless the action is on a trusted path/resource.
392    UnlessTrusted,
393    /// Only ask after a tool call fails.
394    OnFailure,
395    /// Ask every time a tool call is requested.
396    OnRequest,
397    /// Reject the action without asking, with details on which categories are blocked.
398    Reject {
399        sandbox_approval: bool,
400        rules: bool,
401        mcp_elicitations: bool,
402    },
403    /// Never ask; auto-approve all actions.
404    Never,
405}
406
407/// Classification of tool invocation origin.
408#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
409#[serde(rename_all = "snake_case")]
410pub enum ToolKind {
411    /// A built-in function tool.
412    Function,
413    /// An MCP (Model Context Protocol) tool.
414    Mcp,
415}
416
417/// Parameters for executing a local shell command.
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct LocalShellParams {
420    /// The shell command to execute.
421    pub command: String,
422    /// Working directory for the command.
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub cwd: Option<String>,
425    /// Timeout in milliseconds.
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub timeout_ms: Option<u64>,
428}
429
430/// The payload of a tool call, discriminated by tool type.
431#[derive(Debug, Clone, Serialize, Deserialize)]
432#[serde(tag = "type", rename_all = "snake_case")]
433pub enum ToolPayload {
434    /// A built-in function call with JSON-encoded arguments.
435    Function { arguments: String },
436    /// A custom tool invocation with a free-form input string.
437    Custom { input: String },
438    /// A local shell command execution.
439    LocalShell { params: LocalShellParams },
440    /// An MCP tool invocation targeting a specific server and tool.
441    Mcp {
442        server: String,
443        tool: String,
444        raw_arguments: Value,
445        #[serde(skip_serializing_if = "Option::is_none")]
446        raw_tool_call_id: Option<String>,
447    },
448}
449
450/// The result of a tool call, discriminated by tool type.
451#[derive(Debug, Clone, Serialize, Deserialize)]
452#[serde(tag = "type", rename_all = "snake_case")]
453pub enum ToolOutput {
454    /// Result of a built-in function call.
455    Function {
456        /// The output body, if any.
457        #[serde(skip_serializing_if = "Option::is_none")]
458        body: Option<Value>,
459        /// Whether the call succeeded.
460        success: bool,
461    },
462    /// Result of an MCP tool call.
463    Mcp {
464        /// The result value returned by the MCP server.
465        result: Value,
466    },
467}
468
469/// Action to take for a network policy rule.
470#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
471#[serde(rename_all = "snake_case")]
472pub enum NetworkPolicyRuleAction {
473    /// Allow network access to the host.
474    Allow,
475    /// Deny network access to the host.
476    Deny,
477}
478
479/// A proposed amendment to the network access policy for a specific host.
480#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
481pub struct NetworkPolicyAmendment {
482    /// The host to amend the policy for.
483    pub host: String,
484    /// The action to apply.
485    pub action: NetworkPolicyRuleAction,
486}
487
488/// A user's decision on an approval request.
489#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
490#[serde(tag = "type", rename_all = "snake_case")]
491pub enum ReviewDecision {
492    /// Approve the action.
493    Approved,
494    /// Approve and also amend the execution policy.
495    ApprovedExecpolicyAmendment,
496    /// Approve for the remainder of this session only.
497    ApprovedForSession,
498    /// Approve with a network policy amendment.
499    NetworkPolicyAmendment {
500        host: String,
501        action: NetworkPolicyRuleAction,
502    },
503    /// Deny the action.
504    Denied,
505    /// Abort the entire turn.
506    Abort,
507}
508
509/// Status of an MCP server during startup.
510#[derive(Debug, Clone, Serialize, Deserialize)]
511#[serde(rename_all = "snake_case")]
512pub enum McpStartupStatus {
513    /// The server is in the process of starting.
514    Starting,
515    /// The server is ready to accept requests.
516    Ready,
517    /// The server failed to start.
518    Failed { error: String },
519    /// Startup was cancelled.
520    Cancelled,
521}
522
523/// A progress update for a single MCP server's startup.
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct McpStartupUpdateEvent {
526    /// Name of the MCP server.
527    pub server_name: String,
528    /// Current startup status.
529    pub status: McpStartupStatus,
530}
531
532/// Details of an MCP server that failed to start.
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct McpStartupFailure {
535    /// Name of the MCP server that failed.
536    pub server_name: String,
537    /// Error description.
538    pub error: String,
539}
540
541/// Summary event emitted once all MCP servers have finished starting.
542#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct McpStartupCompleteEvent {
544    /// Servers that started successfully.
545    pub ready: Vec<String>,
546    /// Servers that failed to start.
547    pub failed: Vec<McpStartupFailure>,
548    /// Servers whose startup was cancelled.
549    pub cancelled: Vec<String>,
550}
551
552/// Context about a network access request that requires approval.
553#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct NetworkApprovalContext {
555    /// The host being accessed.
556    pub host: String,
557    /// The network protocol (e.g. `"https"`, `"tcp"`).
558    pub protocol: String,
559}
560
561/// A selectable option presented to the user in a clarification question.
562///
563/// Headless serialization shape for the `request_user_input` model tool,
564/// mirrored after the TUI's `UserInputOption`. Shared by the
565/// [`EventFrame::UserInputRequest`] frame and the [`AppRequest::SubmitUserInput`]
566/// reply path so both surfaces agree on the question schema.
567#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
568pub struct UserInputOptionEvent {
569    /// Short label for the option (also the value submitted when picked).
570    pub label: String,
571    /// Longer description shown alongside the label.
572    pub description: String,
573}
574
575/// A single clarification question posed to the user.
576#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
577pub struct UserInputQuestionEvent {
578    /// Compact header shown as the question title.
579    pub header: String,
580    /// Stable identifier used to correlate answers back to this question.
581    pub id: String,
582    /// The question body.
583    pub question: String,
584    /// 2-4 suggested answers.
585    pub options: Vec<UserInputOptionEvent>,
586    /// When `true`, the client should also offer a free-text response.
587    #[serde(default)]
588    pub allow_free_text: bool,
589    /// When `true`, the user may select more than one option.
590    #[serde(default)]
591    pub multi_select: bool,
592}
593
594/// An event requesting structured user input via a model-tool call.
595///
596/// Sibling of [`ExecApprovalRequestEvent`] for the clarification-question
597/// flow. Emitted fire-and-return by `Runtime::invoke_tool` when the model
598/// invokes `request_user_input` in a headless context.
599#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
600pub struct UserInputRequestEvent {
601    /// Identifier of the tool call requesting input.
602    pub call_id: String,
603    /// The turn during which the request was made.
604    pub turn_id: String,
605    /// Unique identifier for this user-input request (clients reply with it).
606    pub request_id: String,
607    /// 1-3 questions to present.
608    pub questions: Vec<UserInputQuestionEvent>,
609}
610
611/// One answer to a clarification question.
612#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
613pub struct UserInputAnswerEvent {
614    /// The `id` of the question this answer corresponds to.
615    pub id: String,
616    /// The selected option's label, or `"Other"` for a free-text response.
617    pub label: String,
618    /// The resolved value (option label, or the typed free-text).
619    pub value: String,
620}
621
622/// An event requesting user approval for a command execution or patch application.
623#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct ExecApprovalRequestEvent {
625    /// Identifier of the tool call requesting approval.
626    pub call_id: String,
627    /// Unique identifier for this approval request.
628    pub approval_id: String,
629    /// The turn during which the request was made.
630    pub turn_id: String,
631    /// The command that would be executed.
632    pub command: String,
633    /// The working directory for the command.
634    pub cwd: String,
635    /// Human-readable reason why approval is needed.
636    pub reason: String,
637    /// Policy rule that matched this approval request, when available.
638    #[serde(default, skip_serializing_if = "Option::is_none")]
639    pub matched_rule: Option<Box<str>>,
640    /// Network context if the approval involves network access.
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub network_approval_context: Option<NetworkApprovalContext>,
643    /// Proposed execution policy rule amendments.
644    #[serde(default)]
645    pub proposed_execpolicy_amendment: Vec<String>,
646    /// Proposed network policy amendments.
647    #[serde(default)]
648    pub proposed_network_policy_amendments: Vec<NetworkPolicyAmendment>,
649    /// Additional permissions being requested.
650    #[serde(default)]
651    pub additional_permissions: Vec<String>,
652    /// The set of decisions the user can choose from.
653    #[serde(default)]
654    pub available_decisions: Vec<ReviewDecision>,
655}
656
657/// The channel a response delta is being written to.
658#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
659#[serde(rename_all = "snake_case")]
660pub enum ResponseChannel {
661    /// The main visible text output.
662    #[default]
663    Text,
664    /// Internal reasoning / chain-of-thought output.
665    Reasoning,
666}
667
668impl ResponseChannel {
669    /// Returns `true` if this is the `Text` channel.
670    pub const fn is_text(&self) -> bool {
671        matches!(self, ResponseChannel::Text)
672    }
673}
674
675/// A user's approval decision sent in response to an approval request.
676#[derive(Debug, Clone, Serialize, Deserialize)]
677pub struct ApprovalDecisionRequest {
678    /// The decision identifier (e.g. `"approved"`, `"denied"`).
679    pub decision: String,
680    /// Whether to remember this decision for future similar requests.
681    #[serde(default)]
682    pub remember: bool,
683}
684
685/// A single streaming event frame emitted during agent execution.
686///
687/// Events are tagged by the `event` field and cover the full lifecycle of a
688/// turn: response streaming, tool calls, MCP lifecycle, command execution,
689/// patch application, approvals, and errors.
690#[derive(Debug, Clone, Serialize, Deserialize)]
691#[serde(tag = "event", rename_all = "snake_case")]
692pub enum EventFrame {
693    /// A new model response has started.
694    ResponseStart { response_id: String },
695    /// A incremental text delta for an in-progress response.
696    ResponseDelta {
697        response_id: String,
698        delta: String,
699        #[serde(default, skip_serializing_if = "ResponseChannel::is_text")]
700        channel: ResponseChannel,
701    },
702    /// The model response has finished.
703    ResponseEnd { response_id: String },
704    /// A tool call has begun.
705    ToolCallStart {
706        response_id: String,
707        tool_name: String,
708        arguments: Value,
709    },
710    /// A tool call has completed and produced a result.
711    ToolCallResult {
712        response_id: String,
713        tool_name: String,
714        output: Value,
715    },
716    /// Progress update for an MCP server starting up.
717    McpStartupUpdate { update: McpStartupUpdateEvent },
718    /// All MCP servers have finished starting.
719    McpStartupComplete { summary: McpStartupCompleteEvent },
720    /// An MCP tool call has begun.
721    McpToolCallBegin {
722        server_name: String,
723        tool_name: String,
724    },
725    /// An MCP tool call has finished.
726    McpToolCallEnd {
727        server_name: String,
728        tool_name: String,
729        ok: bool,
730    },
731    /// User approval is needed for a command execution.
732    ExecApprovalRequest { request: ExecApprovalRequestEvent },
733    /// User approval is needed for applying a patch.
734    ApplyPatchApprovalRequest { request: ExecApprovalRequestEvent },
735    /// A model tool is requesting structured clarification input from the user.
736    ///
737    /// Headless sibling of the TUI's `request_user_input` modal flow.
738    /// `request_id` correlates with an [`AppRequest::SubmitUserInput`] reply.
739    UserInputRequest { request: UserInputRequestEvent },
740    /// An MCP server is requesting user input (elicitation).
741    ElicitationRequest {
742        server_name: String,
743        request_id: String,
744        prompt: String,
745    },
746    /// A command has started executing.
747    ExecCommandBegin { command: String, cwd: String },
748    /// Incremental output from a running command.
749    ExecCommandOutputDelta { command: String, delta: String },
750    /// A command has finished executing.
751    ExecCommandEnd { command: String, exit_code: i32 },
752    /// A patch has started being applied to a file.
753    PatchApplyBegin { path: String },
754    /// A patch has finished being applied.
755    PatchApplyEnd { path: String, ok: bool },
756    /// A new turn has started within a thread.
757    TurnStarted { turn_id: String },
758    /// A turn has completed successfully.
759    TurnComplete { turn_id: String },
760    /// A turn was aborted before completion.
761    TurnAborted { turn_id: String, reason: String },
762    /// A thread goal was set or updated.
763    ThreadGoalUpdated { goal: ThreadGoal },
764    /// A thread goal was cleared.
765    ThreadGoalCleared { thread_id: String },
766    /// An error occurred during processing.
767    Error {
768        response_id: String,
769        message: String,
770    },
771}