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