返回 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 engine_owner;
15 pub mod event_msg;
16 pub mod fleet;
17 pub mod ids;
18 pub mod journal;
19 pub mod op;
20 pub mod runtime;
21 pub mod workroom;
22
23 /// Common trait for lifecycle status enums across the protocol layer.
24 ///
25 /// Every status enum — thread, goal, fleet run, worker, and job status —
26 /// implements this trait so generic code can ask three universal questions
27 /// without matching on every variant.
28 pub trait Status {
29 /// Returns `true` when this status represents a final, non-progressable state
30 /// (e.g. Completed, Failed, Cancelled, Archived, Retired).
31 fn is_terminal(&self) -> bool;
32
33 /// Returns `true` when work is currently in-flight
34 /// (e.g. Running, Active, Busy, Queued, Pending).
35 fn is_active(&self) -> bool;
36
37 /// Returns `true` when the item has been explicitly paused by the user
38 /// or system (e.g. Paused).
39 fn is_paused(&self) -> bool;
40 }
41
42 /// A single message entry in a conversation thread.
43 ///
44 /// Messages form a tree structure via [`parent_entry_id`](Self::parent_entry_id),
45 /// enabling conversation branching and forking.
46 #[derive(Debug, Clone, Serialize, Deserialize)]
47 pub struct MessageRecord {
48 /// Auto-incremented unique identifier for this message.
49 pub id: i64,
50 /// ID of the thread this message belongs to.
51 pub thread_id: String,
52 /// Role of the message sender (e.g. `"user"`, `"assistant"`, `"system"`).
53 pub role: String,
54 /// Text content of the message.
55 pub content: String,
56 /// Optional structured item payload (tool calls, tool results, etc.).
57 pub item: Option<Value>,
58 /// Unix timestamp (seconds) when the message was created.
59 pub created_at: i64,
60 /// ID of the parent message, forming a tree structure. `None` for root messages.
61 pub parent_entry_id: Option<i64>,
62 }
63
64 /// A complete immutable legacy SQLite graph, not a limit or active-branch projection.
65 #[derive(Debug, Clone, Serialize, Deserialize)]
66 #[serde(deny_unknown_fields)]
67 pub struct LegacyThreadHistory {
68 pub version: u32,
69 pub state_store_id: String,
70 pub thread_id: String,
71 pub current_leaf_id: Option<i64>,
72 pub messages: Vec<MessageRecord>,
73 /// Captured in the same SQLite snapshot; imported Active goals stay paused.
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub goal: Option<ThreadGoal>,
76 }
77
78 pub const MAX_CANONICAL_HISTORY_ENTRIES: usize = 16_384;
79 pub const MAX_CANONICAL_HISTORY_BYTES: usize = 8 * 1024 * 1024;
80
81 /// An authenticated import proposal carries conversation data only. Routing and
82 /// permissions remain the current owner's create-thread policy.
83 #[derive(Debug, Clone, Serialize, Deserialize)]
84 #[serde(deny_unknown_fields)]
85 pub struct CanonicalHistoryImportRequest {
86 pub version: u32,
87 pub operation_key: String,
88 pub expected_data_dir: PathBuf,
89 pub expected_execution_scope: String,
90 /// Existing bare compatibility links attach their full source graph here;
91 /// the owner verifies the actual bound canonical document, never remints it.
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub target_runtime_thread_id: Option<String>,
94 pub workspace: PathBuf,
95 pub model: Option<String>,
96 pub history: LegacyThreadHistory,
97 }
98
99 /// Durable result of the actual canonical owner operation. Its scope fields are
100 /// captured from RuntimeStoreBinding; historical receipts never authenticate a
101 /// current process or convey credentials.
102 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103 #[serde(deny_unknown_fields)]
104 pub struct CanonicalThreadReceipt {
105 pub version: u32,
106 pub data_dir: PathBuf,
107 pub execution_scope: String,
108 pub operation_key: String,
109 pub request_digest: String,
110 pub history_digest: String,
111 pub runtime_thread_id: String,
112 pub session_id: String,
113 }
114
115 /// A complete read projection from the actual Engine and its existing saved
116 /// journal. `session` retains the established SavedSession wire shape, including
117 /// every branch; it is data, never a policy or operation-commit receipt.
118 #[derive(Debug, Clone, Serialize, Deserialize)]
119 #[serde(deny_unknown_fields)]
120 pub struct CanonicalThreadSnapshot {
121 pub version: u32,
122 pub data_dir: PathBuf,
123 pub execution_scope: String,
124 pub runtime_thread_id: String,
125 pub saved_session_id: Option<String>,
126 pub saved_document_digest: Option<String>,
127 pub document_digest: String,
128 pub session_goal_digest: String,
129 pub session: Value,
130 }
131
132 /// One client-captured intent. The key is retained after an uncertain reply;
133 /// retrying it never allocates another canonical identity.
134 #[derive(Debug, Clone, Serialize, Deserialize)]
135 #[serde(deny_unknown_fields)]
136 pub struct CanonicalThreadMutationRequest {
137 pub version: u32,
138 pub operation_key: String,
139 pub expected_data_dir: PathBuf,
140 pub expected_execution_scope: String,
141 pub workspace: PathBuf,
142 pub mutation: CanonicalThreadMutation,
143 }
144
145 #[derive(Debug, Clone, Serialize, Deserialize)]
146 #[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
147 pub enum CanonicalThreadMutation {
148 /// The existing CreateThreadRequest shape, checked by the owner. Conversation
149 /// data and an imported document never supply a routing or permission ceiling.
150 Create { config: Value },
151 Resume {
152 source: CanonicalHistorySource,
153 #[serde(default)]
154 options: CanonicalHistoryOptions,
155 },
156 Fork {
157 source: CanonicalHistorySource,
158 #[serde(default)]
159 options: CanonicalHistoryOptions,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 selected_entry_id: Option<String>,
162 },
163 }
164
165 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
166 #[serde(deny_unknown_fields)]
167 pub struct CanonicalHistoryOptions {
168 #[serde(default, skip_serializing_if = "Vec::is_empty")]
169 pub offered_history: Vec<Value>,
170 #[serde(default, skip_serializing_if = "Value::is_null")]
171 pub overrides: Value,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub source_path: Option<PathBuf>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub expected_session_goal_digest: Option<String>,
176 }
177
178 #[derive(Debug, Clone, Serialize, Deserialize)]
179 #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
180 pub enum CanonicalHistorySource {
181 Thread {
182 runtime_thread_id: String,
183 expected_document_digest: String,
184 },
185 /// Local mounted handoff supplies the already protected complete document;
186 /// the actual owner reopens it and compares the same whole-document digest.
187 SavedSession {
188 session: Value,
189 expected_document_digest: String,
190 },
191 }
192
193 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194 #[serde(rename_all = "snake_case")]
195 pub enum CanonicalThreadOperationKind {
196 Create,
197 Resume,
198 Fork,
199 }
200
201 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202 #[serde(deny_unknown_fields)]
203 pub struct CanonicalThreadOperationAssociation {
204 pub kind: CanonicalThreadOperationKind,
205 pub source_runtime_thread_id: Option<String>,
206 pub source_session_id: Option<String>,
207 }
208
209 #[derive(Debug, Clone, Serialize, Deserialize)]
210 #[serde(deny_unknown_fields)]
211 pub struct CanonicalThreadOperationLookup {
212 pub version: u32,
213 pub operation_key: String,
214 pub expected_data_dir: PathBuf,
215 pub expected_execution_scope: String,
216 pub workspace: PathBuf,
217 }
218
219 /// Explicit completion of an already-prepared retained intent, with no source proposal.
220 #[derive(Debug, Clone, Serialize, Deserialize)]
221 #[serde(deny_unknown_fields)]
222 pub struct CanonicalThreadOperationRecovery {
223 pub operation: CanonicalThreadOperationLookup,
224 pub association: CanonicalThreadOperationAssociation,
225 }
226
227 #[derive(Debug, Clone, Serialize, Deserialize)]
228 #[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
229 pub enum CanonicalThreadOperationStatus {
230 Absent,
231 Pending {
232 receipt: CanonicalThreadReceipt,
233 association: CanonicalThreadOperationAssociation,
234 },
235 Committed {
236 receipt: CanonicalThreadReceipt,
237 association: CanonicalThreadOperationAssociation,
238 },
239 }
240
241 /// Closed routing receipt captured from the held Runtime owner, never a bearer.
242 /// Authentication also requires the actual connected kernel peer identity.
243 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244 #[serde(deny_unknown_fields)]
245 pub struct RuntimeOwnerReceipt {
246 pub version: u32,
247 pub data_dir: PathBuf,
248 pub execution_scope: String,
249 pub lease_generation: String,
250 pub pid: u32,
251 pub process_start: String,
252 pub principal: String,
253 pub socket_path: PathBuf,
254 pub config_path: Option<PathBuf>,
255 }
256
257 #[derive(Debug, Clone, Serialize, Deserialize)]
258 pub struct Envelope<T> {
259 pub request_id: String,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub thread_id: Option<String>,
262 pub body: T,
263 }
264
265 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
266 #[serde(rename_all = "snake_case")]
267 pub enum ThreadStatus {
268 Running,
269 Idle,
270 Completed,
271 Failed,
272 Paused,
273 Archived,
274 }
275
276 impl Status for ThreadStatus {
277 fn is_terminal(&self) -> bool {
278 matches!(self, Self::Completed | Self::Failed | Self::Archived)
279 }
280 fn is_active(&self) -> bool {
281 matches!(self, Self::Running)
282 }
283 fn is_paused(&self) -> bool {
284 matches!(self, Self::Paused)
285 }
286 }
287
288 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
289 #[serde(rename_all = "snake_case")]
290 pub enum SessionSource {
291 Interactive,
292 Resume,
293 Fork,
294 Api,
295 Unknown,
296 }
297
298 #[derive(Debug, Clone, Serialize, Deserialize)]
299 pub struct Thread {
300 pub id: String,
301 pub preview: String,
302 pub ephemeral: bool,
303 pub model_provider: String,
304 pub created_at: i64,
305 pub updated_at: i64,
306 pub status: ThreadStatus,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub path: Option<PathBuf>,
309 pub cwd: PathBuf,
310 pub cli_version: String,
311 pub source: SessionSource,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 pub name: Option<String>,
314 }
315
316 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
317 #[serde(rename_all = "snake_case")]
318 pub enum ThreadGoalStatus {
319 Active,
320 Paused,
321 Blocked,
322 UsageLimited,
323 BudgetLimited,
324 Complete,
325 }
326
327 impl Status for ThreadGoalStatus {
328 fn is_terminal(&self) -> bool {
329 matches!(self, Self::Complete)
330 }
331 fn is_active(&self) -> bool {
332 matches!(self, Self::Active)
333 }
334 fn is_paused(&self) -> bool {
335 matches!(self, Self::Paused)
336 }
337 }
338
339 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
340 pub struct ThreadGoal {
341 pub thread_id: String,
342 pub goal_id: String,
343 pub objective: String,
344 pub status: ThreadGoalStatus,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub token_budget: Option<i64>,
347 pub tokens_used: i64,
348 pub time_used_seconds: i64,
349 pub continuation_count: i64,
350 pub created_at: i64,
351 pub updated_at: i64,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub last_gap_fingerprint: Option<String>,
354 #[serde(default)]
355 pub repeated_gap_count: u32,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub last_gap_pass: Option<u32>,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub pause_reason: Option<GoalPauseReason>,
360 }
361
362 /// Why an unfinished goal is paused. Shared by every durable host projection.
363 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
364 #[serde(rename_all = "snake_case")]
365 pub enum GoalPauseReason {
366 User,
367 Backoff,
368 NoProgress,
369 UsageLimit,
370 BudgetLimit,
371 }
372
373 impl GoalPauseReason {
374 #[must_use]
375 pub fn label(self) -> &'static str {
376 match self {
377 Self::User => "user",
378 Self::Backoff => "run limit",
379 Self::NoProgress => "no progress",
380 Self::UsageLimit => "usage limit",
381 Self::BudgetLimit => "budget limit",
382 }
383 }
384 }
385
386 /// Validate the compact stall history without retaining verifier prose.
387 /// Legacy records with the entire history absent start with an empty window.
388 pub const MAX_REPEATED_GAP_COUNT: u32 = 3;
389
390 pub fn validate_goal_stall_state(
391 fingerprint: Option<&str>,
392 count: u32,
393 pass: Option<u32>,
394 continuation_count: u32,
395 ) -> Result<(), &'static str> {
396 match (fingerprint, count, pass) {
397 (None, 0, None) => Ok(()),
398 (Some(digest), 1..=MAX_REPEATED_GAP_COUNT, Some(pass))
399 if digest.len() == 64
400 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
401 && pass <= continuation_count
402 && count <= pass.saturating_add(1) =>
403 {
404 Ok(())
405 }
406 _ => Err("invalid persisted goal stall history"),
407 }
408 }
409
410 impl ThreadGoal {
411 pub fn validate_stall_state(&self) -> Result<(), &'static str> {
412 validate_goal_stall_state(
413 self.last_gap_fingerprint.as_deref(),
414 self.repeated_gap_count,
415 self.last_gap_pass,
416 u32::try_from(self.continuation_count.max(0)).unwrap_or(u32::MAX),
417 )
418 }
419
420 /// Restore a durably impossible record as paused. The engine pauses
421 /// NoProgress in the same locked mutation that fills the stall window, so
422 /// a persisted record that is still Active at the ceiling is corrupt
423 /// (e.g. a crash between the counter write and the pause). Returns true
424 /// when the record was healed.
425 pub fn normalize_restored_stall_state(&mut self) -> bool {
426 if matches!(self.status, ThreadGoalStatus::Active)
427 && self.repeated_gap_count >= MAX_REPEATED_GAP_COUNT
428 {
429 self.status = ThreadGoalStatus::Paused;
430 self.pause_reason = Some(GoalPauseReason::NoProgress);
431 true
432 } else {
433 false
434 }
435 }
436 }
437
438 #[derive(Debug, Clone, Serialize, Deserialize)]
439 pub struct ThreadStartParams {
440 /// Captured once for this user intent and retained for an uncertain reply.
441 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub operation_key: Option<String>,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 pub model: Option<String>,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub model_provider: Option<String>,
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub cwd: Option<PathBuf>,
449 #[serde(default)]
450 pub persist_extended_history: bool,
451 }
452
453 #[derive(Debug, Clone, Serialize, Deserialize)]
454 pub struct ThreadResumeParams {
455 /// Captured once for this user intent and retained for an uncertain reply.
456 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub operation_key: Option<String>,
458 pub thread_id: String,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub history: Option<Vec<Value>>,
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub path: Option<PathBuf>,
463 #[serde(skip_serializing_if = "Option::is_none")]
464 pub model: Option<String>,
465 #[serde(skip_serializing_if = "Option::is_none")]
466 pub model_provider: Option<String>,
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub cwd: Option<PathBuf>,
469 #[serde(skip_serializing_if = "Option::is_none")]
470 pub approval_policy: Option<String>,
471 #[serde(skip_serializing_if = "Option::is_none")]
472 pub sandbox: Option<String>,
473 #[serde(skip_serializing_if = "Option::is_none")]
474 pub config: Option<Value>,
475 #[serde(skip_serializing_if = "Option::is_none")]
476 pub base_instructions: Option<String>,
477 #[serde(skip_serializing_if = "Option::is_none")]
478 pub developer_instructions: Option<String>,
479 #[serde(skip_serializing_if = "Option::is_none")]
480 pub personality: Option<String>,
481 #[serde(default)]
482 pub persist_extended_history: bool,
483 }
484
485 #[derive(Debug, Clone, Serialize, Deserialize)]
486 pub struct ThreadForkParams {
487 /// Captured once for this user intent and retained for an uncertain reply.
488 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub operation_key: Option<String>,
490 pub thread_id: String,
491 #[serde(skip_serializing_if = "Option::is_none")]
492 pub path: Option<PathBuf>,
493 #[serde(skip_serializing_if = "Option::is_none")]
494 pub model: Option<String>,
495 #[serde(skip_serializing_if = "Option::is_none")]
496 pub model_provider: Option<String>,
497 #[serde(skip_serializing_if = "Option::is_none")]
498 pub cwd: Option<PathBuf>,
499 #[serde(skip_serializing_if = "Option::is_none")]
500 pub approval_policy: Option<String>,
501 #[serde(skip_serializing_if = "Option::is_none")]
502 pub sandbox: Option<String>,
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub config: Option<Value>,
505 #[serde(skip_serializing_if = "Option::is_none")]
506 pub base_instructions: Option<String>,
507 #[serde(skip_serializing_if = "Option::is_none")]
508 pub developer_instructions: Option<String>,
509 #[serde(default)]
510 pub persist_extended_history: bool,
511 }
512
513 #[derive(Debug, Clone, Serialize, Deserialize)]
514 pub struct ThreadListParams {
515 #[serde(default)]
516 pub include_archived: bool,
517 #[serde(skip_serializing_if = "Option::is_none")]
518 pub limit: Option<usize>,
519 }
520
521 #[derive(Debug, Clone, Serialize, Deserialize)]
522 pub struct ThreadReadParams {
523 pub thread_id: String,
524 }
525
526 #[derive(Debug, Clone, Serialize, Deserialize)]
527 pub struct ThreadSetNameParams {
528 pub thread_id: String,
529 pub name: String,
530 }
531
532 #[derive(Debug, Clone, Serialize, Deserialize)]
533 pub struct ThreadGoalSetParams {
534 pub thread_id: String,
535 pub objective: String,
536 #[serde(skip_serializing_if = "Option::is_none")]
537 pub token_budget: Option<i64>,
538 }
539
540 #[derive(Debug, Clone, Serialize, Deserialize)]
541 pub struct ThreadGoalGetParams {
542 pub thread_id: String,
543 }
544
545 #[derive(Debug, Clone, Serialize, Deserialize)]
546 pub struct ThreadGoalClearParams {
547 pub thread_id: String,
548 }
549
550 #[derive(Debug, Clone, Serialize, Deserialize)]
551 pub struct ThreadGoalProgressParams {
552 pub thread_id: String,
553 #[serde(default)]
554 pub token_delta: i64,
555 #[serde(default)]
556 pub time_delta_seconds: i64,
557 #[serde(default)]
558 pub record_continuation: bool,
559 }
560
561 #[derive(Debug, Clone, Serialize, Deserialize)]
562 #[serde(tag = "kind", rename_all = "snake_case")]
563 pub enum ThreadRequest {
564 Create {
565 #[serde(default)]
566 metadata: Value,
567 },
568 Start(ThreadStartParams),
569 Resume(ThreadResumeParams),
570 Fork(ThreadForkParams),
571 List(ThreadListParams),
572 Read(ThreadReadParams),
573 SetName(ThreadSetNameParams),
574 GoalSet(ThreadGoalSetParams),
575 GoalGet(ThreadGoalGetParams),
576 GoalClear(ThreadGoalClearParams),
577 GoalRecordProgress(ThreadGoalProgressParams),
578 Archive {
579 thread_id: String,
580 },
581 Unarchive {
582 thread_id: String,
583 },
584 Message {
585 thread_id: String,
586 input: String,
587 #[serde(default, skip_serializing_if = "Vec::is_empty")]
588 images: Vec<runtime::RuntimeImageInput>,
589 #[serde(
590 default,
591 rename = "maxOutputTokens",
592 alias = "max_output_tokens",
593 skip_serializing_if = "Option::is_none"
594 )]
595 max_output_tokens: Option<std::num::NonZeroU32>,
596 },
597 }
598
599 /// Response to a [`ThreadRequest`].
600 #[derive(Debug, Clone, Serialize, Deserialize)]
601 pub struct ThreadResponse {
602 /// The thread this response pertains to.
603 pub thread_id: String,
604 /// Human-readable status string (e.g. `"ok"`, `"error"`).
605 pub status: String,
606 /// The thread details, when a single thread is returned.
607 #[serde(skip_serializing_if = "Option::is_none")]
608 pub thread: Option<Thread>,
609 /// List of threads, populated by `List` requests.
610 #[serde(default)]
611 pub threads: Vec<Thread>,
612 /// Thread goal returned by goal get/set requests.
613 #[serde(skip_serializing_if = "Option::is_none")]
614 pub goal: Option<ThreadGoal>,
615 /// The model used for the thread, if applicable.
616 #[serde(skip_serializing_if = "Option::is_none")]
617 pub model: Option<String>,
618 /// The model provider used for the thread.
619 #[serde(skip_serializing_if = "Option::is_none")]
620 pub model_provider: Option<String>,
621 /// The working directory of the thread.
622 #[serde(skip_serializing_if = "Option::is_none")]
623 pub cwd: Option<PathBuf>,
624 /// The active approval policy.
625 #[serde(skip_serializing_if = "Option::is_none")]
626 pub approval_policy: Option<String>,
627 /// The active sandbox configuration.
628 #[serde(skip_serializing_if = "Option::is_none")]
629 pub sandbox: Option<String>,
630 /// Streaming events associated with this response.
631 #[serde(default)]
632 pub events: Vec<EventFrame>,
633 /// Arbitrary additional response data.
634 #[serde(default)]
635 pub data: Value,
636 }
637
638 /// Application-level requests that are not tied to a specific thread.
639 #[derive(Debug, Clone, Serialize, Deserialize)]
640 #[serde(tag = "kind", rename_all = "snake_case")]
641 pub enum AppRequest {
642 /// Query the server's capabilities.
643 Capabilities,
644 /// Read a configuration value by key.
645 ConfigGet { key: String },
646 /// Set a configuration key to a value.
647 ConfigSet { key: String, value: String },
648 /// Remove a configuration key.
649 ConfigUnset { key: String },
650 /// List all configuration entries.
651 ConfigList,
652 /// Reload configuration from disk and apply to the live runtime.
653 ///
654 /// Re-reads both `config.toml` and the sibling `permissions.toml`,
655 /// refreshing the live `Runtime.config` and `Runtime.exec_policy`
656 /// so headless clients can pick up external config-file *and*
657 /// permission-rule edits without restarting.
658 ///
659 /// Mirrors the TUI `reload_runtime_config` codepath for everything
660 /// reachable from the headless `Runtime`. MCP server connections
661 /// are not refreshed — changing `mcp_config_path` or the referenced
662 /// `mcp.json` still requires a headless-runtime restart. The TUI's
663 /// explicit `/mcp reload` operation is not part of this protocol path.
664 ConfigReload,
665 /// List available models.
666 Models,
667 /// List threads that are currently loaded in memory.
668 ThreadLoadedList,
669 /// Submit answers to a prior [`EventFrame::UserInputRequest`].
670 ///
671 /// `request_id` must match a pending clarification request. Headless
672 /// clients use this to return the user's selections back to the runtime.
673 SubmitUserInput {
674 request_id: String,
675 answers: Vec<UserInputAnswerEvent>,
676 },
677 }
678
679 /// Response to an [`AppRequest`].
680 #[derive(Debug, Clone, Serialize, Deserialize)]
681 pub struct AppResponse {
682 /// Whether the request succeeded.
683 pub ok: bool,
684 /// The response payload.
685 pub data: Value,
686 /// Streaming events associated with this response.
687 #[serde(default)]
688 pub events: Vec<EventFrame>,
689 }
690
691 /// A simple prompt request that sends text to the model and returns output.
692 #[derive(Debug, Clone, Serialize, Deserialize)]
693 pub struct PromptRequest {
694 #[serde(
695 default,
696 rename = "maxOutputTokens",
697 alias = "max_output_tokens",
698 skip_serializing_if = "Option::is_none"
699 )]
700 pub max_output_tokens: Option<std::num::NonZeroU32>,
701 /// Optional thread context for the prompt.
702 #[serde(skip_serializing_if = "Option::is_none")]
703 pub thread_id: Option<String>,
704 /// The prompt text.
705 pub prompt: String,
706 #[serde(default, skip_serializing_if = "Vec::is_empty")]
707 pub images: Vec<runtime::RuntimeImageInput>,
708 /// Model override, or the default if omitted.
709 #[serde(skip_serializing_if = "Option::is_none")]
710 pub model: Option<String>,
711 }
712
713 /// Response to a [`PromptRequest`].
714 #[derive(Debug, Clone, Serialize, Deserialize)]
715 pub struct PromptResponse {
716 /// The model's output text.
717 pub output: String,
718 /// The model that produced the output.
719 pub model: String,
720 /// Streaming events associated with this response.
721 #[serde(default)]
722 pub events: Vec<EventFrame>,
723 }
724
725 /// Policy controlling when the agent must ask the user for approval before acting.
726 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
727 #[serde(rename_all = "snake_case")]
728 pub enum AskForApproval {
729 /// Ask for approval unless the action is on a trusted path/resource.
730 UnlessTrusted,
731 /// Only ask after a tool call fails.
732 OnFailure,
733 /// Ask every time a tool call is requested.
734 OnRequest,
735 /// Reject the action without asking, with details on which categories are blocked.
736 Reject {
737 sandbox_approval: bool,
738 rules: bool,
739 mcp_elicitations: bool,
740 },
741 /// Never ask; auto-approve all actions.
742 Never,
743 }
744
745 /// Classification of tool invocation origin.
746 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
747 #[serde(rename_all = "snake_case")]
748 pub enum ToolKind {
749 /// A built-in function tool.
750 Function,
751 /// An MCP (Model Context Protocol) tool.
752 Mcp,
753 }
754
755 /// Parameters for executing a local shell command.
756 #[derive(Debug, Clone, Serialize, Deserialize)]
757 pub struct LocalShellParams {
758 /// The shell command to execute.
759 pub command: String,
760 /// Working directory for the command.
761 #[serde(skip_serializing_if = "Option::is_none")]
762 pub cwd: Option<String>,
763 /// Timeout in milliseconds.
764 #[serde(skip_serializing_if = "Option::is_none")]
765 pub timeout_ms: Option<u64>,
766 }
767
768 /// The payload of a tool call, discriminated by tool type.
769 #[derive(Debug, Clone, Serialize, Deserialize)]
770 #[serde(tag = "type", rename_all = "snake_case")]
771 pub enum ToolPayload {
772 /// A built-in function call with JSON-encoded arguments.
773 Function { arguments: String },
774 /// A custom tool invocation with a free-form input string.
775 Custom { input: String },
776 /// A local shell command execution.
777 LocalShell { params: LocalShellParams },
778 /// An MCP tool invocation targeting a specific server and tool.
779 Mcp {
780 server: String,
781 tool: String,
782 raw_arguments: Value,
783 #[serde(skip_serializing_if = "Option::is_none")]
784 raw_tool_call_id: Option<String>,
785 },
786 }
787
788 /// The result of a tool call, discriminated by tool type.
789 #[derive(Debug, Clone, Serialize, Deserialize)]
790 #[serde(tag = "type", rename_all = "snake_case")]
791 pub enum ToolOutput {
792 /// Result of a built-in function call.
793 Function {
794 /// The output body, if any.
795 #[serde(skip_serializing_if = "Option::is_none")]
796 body: Option<Value>,
797 /// Whether the call succeeded.
798 success: bool,
799 },
800 /// Result of an MCP tool call.
801 Mcp {
802 /// The result value returned by the MCP server.
803 result: Value,
804 },
805 }
806
807 impl ToolOutput {
808 /// Returns the tool's application-level success independently of transport.
809 ///
810 /// MCP success requires the top-level `isError` field to be omitted or the
811 /// literal boolean `false`; malformed present metadata fails closed.
812 pub fn success(&self) -> bool {
813 match self {
814 Self::Function { success, .. } => *success,
815 Self::Mcp { result } => {
816 matches!(result.get("isError"), None | Some(Value::Bool(false)))
817 }
818 }
819 }
820 }
821
822 /// Action to take for a network policy rule.
823 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
824 #[serde(rename_all = "snake_case")]
825 pub enum NetworkPolicyRuleAction {
826 /// Allow network access to the host.
827 Allow,
828 /// Deny network access to the host.
829 Deny,
830 }
831
832 /// A proposed amendment to the network access policy for a specific host.
833 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
834 pub struct NetworkPolicyAmendment {
835 /// The host to amend the policy for.
836 pub host: String,
837 /// The action to apply.
838 pub action: NetworkPolicyRuleAction,
839 }
840
841 /// A user's decision on an approval request.
842 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
843 #[serde(tag = "type", rename_all = "snake_case")]
844 pub enum ReviewDecision {
845 /// Approve the action.
846 Approved,
847 /// Approve and also amend the execution policy.
848 ApprovedExecpolicyAmendment,
849 /// Approve for the remainder of this session only.
850 ApprovedForSession,
851 /// Approve with a network policy amendment.
852 NetworkPolicyAmendment {
853 host: String,
854 action: NetworkPolicyRuleAction,
855 },
856 /// Deny the action.
857 Denied,
858 /// Abort the entire turn.
859 Abort,
860 }
861
862 /// Status of an MCP server during startup.
863 #[derive(Debug, Clone, Serialize, Deserialize)]
864 #[serde(rename_all = "snake_case")]
865 pub enum McpStartupStatus {
866 /// The server is in the process of starting.
867 Starting,
868 /// The server is ready to accept requests.
869 Ready,
870 /// The server failed to start.
871 Failed { error: String },
872 /// Startup was cancelled.
873 Cancelled,
874 }
875
876 /// A progress update for a single MCP server's startup.
877 #[derive(Debug, Clone, Serialize, Deserialize)]
878 pub struct McpStartupUpdateEvent {
879 /// Name of the MCP server.
880 pub server_name: String,
881 /// Current startup status.
882 pub status: McpStartupStatus,
883 }
884
885 /// Details of an MCP server that failed to start.
886 #[derive(Debug, Clone, Serialize, Deserialize)]
887 pub struct McpStartupFailure {
888 /// Name of the MCP server that failed.
889 pub server_name: String,
890 /// Error description.
891 pub error: String,
892 }
893
894 /// Summary event emitted once all MCP servers have finished starting.
895 #[derive(Debug, Clone, Serialize, Deserialize)]
896 pub struct McpStartupCompleteEvent {
897 /// Servers that started successfully.
898 pub ready: Vec<String>,
899 /// Servers that failed to start.
900 pub failed: Vec<McpStartupFailure>,
901 /// Servers whose startup was cancelled.
902 pub cancelled: Vec<String>,
903 }
904
905 /// Context about a network access request that requires approval.
906 #[derive(Debug, Clone, Serialize, Deserialize)]
907 pub struct NetworkApprovalContext {
908 /// The host being accessed.
909 pub host: String,
910 /// The network protocol (e.g. `"https"`, `"tcp"`).
911 pub protocol: String,
912 }
913
914 /// A selectable option presented to the user in a clarification question.
915 ///
916 /// Headless serialization shape for the `request_user_input` model tool,
917 /// mirrored after the TUI's `UserInputOption`. Shared by the
918 /// [`EventFrame::UserInputRequest`] frame and the [`AppRequest::SubmitUserInput`]
919 /// reply path so both surfaces agree on the question schema.
920 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
921 pub struct UserInputOptionEvent {
922 /// Short label for the option (also the value submitted when picked).
923 pub label: String,
924 /// Longer description shown alongside the label.
925 pub description: String,
926 }
927
928 /// A single clarification question posed to the user.
929 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
930 pub struct UserInputQuestionEvent {
931 /// Compact header shown as the question title.
932 pub header: String,
933 /// Stable identifier used to correlate answers back to this question.
934 pub id: String,
935 /// The question body.
936 pub question: String,
937 /// 2-4 suggested answers.
938 pub options: Vec<UserInputOptionEvent>,
939 /// When `true`, the client should also offer a free-text response.
940 #[serde(default)]
941 pub allow_free_text: bool,
942 /// When `true`, the user may select more than one option.
943 #[serde(default)]
944 pub multi_select: bool,
945 }
946
947 /// An event requesting structured user input via a model-tool call.
948 ///
949 /// Sibling of [`ExecApprovalRequestEvent`] for the clarification-question
950 /// flow. Emitted fire-and-return by `Runtime::invoke_tool` when the model
951 /// invokes `request_user_input` in a headless context.
952 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
953 pub struct UserInputRequestEvent {
954 /// Identifier of the tool call requesting input.
955 pub call_id: String,
956 /// The turn during which the request was made.
957 pub turn_id: String,
958 /// Unique identifier for this user-input request (clients reply with it).
959 pub request_id: String,
960 /// 1-3 questions to present.
961 pub questions: Vec<UserInputQuestionEvent>,
962 }
963
964 /// One answer to a clarification question.
965 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
966 pub struct UserInputAnswerEvent {
967 /// The `id` of the question this answer corresponds to.
968 pub id: String,
969 /// The selected option's label, or `"Other"` for a free-text response.
970 pub label: String,
971 /// The resolved value (option label, or the typed free-text).
972 pub value: String,
973 }
974
975 /// An event requesting user approval for a command execution or patch application.
976 #[derive(Debug, Clone, Serialize, Deserialize)]
977 pub struct ExecApprovalRequestEvent {
978 /// Identifier of the tool call requesting approval.
979 pub call_id: String,
980 /// Unique identifier for this approval request.
981 pub approval_id: String,
982 /// The turn during which the request was made.
983 pub turn_id: String,
984 /// The command that would be executed.
985 pub command: String,
986 /// The working directory for the command.
987 pub cwd: String,
988 /// Human-readable reason why approval is needed.
989 pub reason: String,
990 /// Policy rule that matched this approval request, when available.
991 #[serde(default, skip_serializing_if = "Option::is_none")]
992 pub matched_rule: Option<Box<str>>,
993 /// Network context if the approval involves network access.
994 #[serde(skip_serializing_if = "Option::is_none")]
995 pub network_approval_context: Option<NetworkApprovalContext>,
996 /// Proposed execution policy rule amendments.
997 #[serde(default)]
998 pub proposed_execpolicy_amendment: Vec<String>,
999 /// Proposed network policy amendments.
1000 #[serde(default)]
1001 pub proposed_network_policy_amendments: Vec<NetworkPolicyAmendment>,
1002 /// Additional permissions being requested.
1003 #[serde(default)]
1004 pub additional_permissions: Vec<String>,
1005 /// The set of decisions the user can choose from.
1006 #[serde(default)]
1007 pub available_decisions: Vec<ReviewDecision>,
1008 }
1009
1010 /// The channel a response delta is being written to.
1011 #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1012 #[serde(rename_all = "snake_case")]
1013 pub enum ResponseChannel {
1014 /// The main visible text output.
1015 #[default]
1016 Text,
1017 /// Internal reasoning / chain-of-thought output.
1018 Reasoning,
1019 }
1020
1021 impl ResponseChannel {
1022 /// Returns `true` if this is the `Text` channel.
1023 pub const fn is_text(&self) -> bool {
1024 matches!(self, ResponseChannel::Text)
1025 }
1026 }
1027
1028 /// A user's approval decision sent in response to an approval request.
1029 #[derive(Debug, Clone, Serialize, Deserialize)]
1030 pub struct ApprovalDecisionRequest {
1031 /// The decision identifier (e.g. `"approved"`, `"denied"`).
1032 pub decision: String,
1033 /// Whether to remember this decision for future similar requests.
1034 #[serde(default)]
1035 pub remember: bool,
1036 }
1037
1038 /// A single streaming event frame emitted during agent execution.
1039 ///
1040 /// Events are tagged by the `event` field and cover the full lifecycle of a
1041 /// turn: response streaming, tool calls, MCP lifecycle, command execution,
1042 /// patch application, approvals, and errors.
1043 #[derive(Debug, Clone, Serialize, Deserialize)]
1044 #[serde(tag = "event", rename_all = "snake_case")]
1045 pub enum EventFrame {
1046 /// A new model response has started.
1047 ResponseStart { response_id: String },
1048 /// A incremental text delta for an in-progress response.
1049 ResponseDelta {
1050 response_id: String,
1051 delta: String,
1052 #[serde(default, skip_serializing_if = "ResponseChannel::is_text")]
1053 channel: ResponseChannel,
1054 },
1055 /// The model response has finished.
1056 ResponseEnd { response_id: String },
1057 /// A tool call has begun.
1058 ToolCallStart {
1059 response_id: String,
1060 tool_name: String,
1061 arguments: Value,
1062 },
1063 /// A tool call has completed and produced a result.
1064 ToolCallResult {
1065 response_id: String,
1066 tool_name: String,
1067 output: Value,
1068 },
1069 /// Progress update for an MCP server starting up.
1070 McpStartupUpdate { update: McpStartupUpdateEvent },
1071 /// All MCP servers have finished starting.
1072 McpStartupComplete { summary: McpStartupCompleteEvent },
1073 /// An MCP tool call has begun.
1074 McpToolCallBegin {
1075 server_name: String,
1076 tool_name: String,
1077 },
1078 /// An MCP tool call has finished.
1079 McpToolCallEnd {
1080 server_name: String,
1081 tool_name: String,
1082 ok: bool,
1083 },
1084 /// User approval is needed for a command execution.
1085 ExecApprovalRequest { request: ExecApprovalRequestEvent },
1086 /// User approval is needed for applying a patch.
1087 ApplyPatchApprovalRequest { request: ExecApprovalRequestEvent },
1088 /// A model tool is requesting structured clarification input from the user.
1089 ///
1090 /// Headless sibling of the TUI's `request_user_input` modal flow.
1091 /// `request_id` correlates with an [`AppRequest::SubmitUserInput`] reply.
1092 UserInputRequest { request: UserInputRequestEvent },
1093 /// An MCP server is requesting user input (elicitation).
1094 ElicitationRequest {
1095 server_name: String,
1096 request_id: String,
1097 prompt: String,
1098 },
1099 /// A command has started executing.
1100 ExecCommandBegin { command: String, cwd: String },
1101 /// Incremental output from a running command.
1102 ExecCommandOutputDelta { command: String, delta: String },
1103 /// A command has finished executing.
1104 ExecCommandEnd { command: String, exit_code: i32 },
1105 /// A patch has started being applied to a file.
1106 PatchApplyBegin { path: String },
1107 /// A patch has finished being applied.
1108 PatchApplyEnd { path: String, ok: bool },
1109 /// A new turn has started within a thread.
1110 TurnStarted { turn_id: String },
1111 /// A turn has completed successfully.
1112 TurnComplete { turn_id: String },
1113 /// A turn was aborted before completion.
1114 TurnAborted { turn_id: String, reason: String },
1115 /// A thread goal was set or updated.
1116 ThreadGoalUpdated { goal: ThreadGoal },
1117 /// A thread goal was cleared.
1118 ThreadGoalCleared { thread_id: String },
1119 /// An error occurred during processing.
1120 Error {
1121 response_id: String,
1122 message: String,
1123 },
1124 }
1125
1126 pub mod request;
1127 pub mod role;
1128
1128 lines RUST