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