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