navi_core/event.rs
1use crate::capability::CapabilityLedgerEntry;
2use crate::goal::types::GoalStatus;
3use crate::patch::PatchProposal;
4use crate::tool::{ToolInvocation, ToolResult};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// One display item in a transient subagent transcript.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct SubagentTranscriptItem {
11 /// Kind of transcript item.
12 pub kind: SubagentTranscriptKind,
13 /// Main one-line item text.
14 pub title: String,
15 /// Optional secondary text, already compacted for UI display.
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub detail: Option<String>,
18 /// Optional success state for completed work.
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub ok: Option<bool>,
21}
22
23/// Display item kind for a transient subagent transcript.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum SubagentTranscriptKind {
26 ToolRequested,
27 ToolCompleted,
28 Text,
29}
30
31/// A versioned runtime event emitted during agent execution.
32///
33/// Wraps a [`RuntimeEventKind`] with a schema version so consumers can handle
34/// forward-compatible event streams.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RuntimeEvent {
37 /// Event schema version. Currently `1`.
38 #[serde(default)]
39 pub version: u32,
40 /// The specific event payload.
41 pub kind: RuntimeEventKind,
42}
43
44impl RuntimeEvent {
45 /// Creates a new event with version 1.
46 pub fn new(kind: RuntimeEventKind) -> Self {
47 Self { version: 1, kind }
48 }
49
50 /// Converts this event into an [`AgentEvent`] if the kind maps to one.
51 ///
52 /// Lifecycle-only events (session started/saved/finished, turn
53 /// started/completed, tool started, context updated) return `None` because
54 /// they have no direct agent-level counterpart.
55 pub fn into_agent_event(self) -> Option<AgentEvent> {
56 self.kind.into_agent_event()
57 }
58}
59
60/// Discriminates the kind of runtime event emitted by the agent loop.
61///
62/// Variants cover the full session lifecycle from start through turn
63/// execution, tool invocation, approval flow, compaction, and error reporting.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub enum RuntimeEventKind {
66 /// A new session has been created.
67 SessionStarted {
68 /// Unique identifier for the session.
69 session_id: String,
70 },
71 /// A new turn within the session has started.
72 TurnStarted {
73 /// Unique identifier for the turn.
74 turn_id: String,
75 },
76 /// A streaming text delta from the assistant.
77 AssistantDelta {
78 /// Incremental text content.
79 text: String,
80 },
81 /// A streaming thinking/reasoning delta from the assistant.
82 AssistantThinkingDelta {
83 /// Incremental thinking content.
84 text: String,
85 },
86 /// The assistant has requested a tool invocation.
87 ToolRequested(ToolInvocation),
88 /// A tool invocation requires user approval before execution.
89 ApprovalRequired(ApprovalRequest),
90 /// An approval request has been resolved (approved or denied).
91 ApprovalResolved(ApprovalDecision),
92 /// A capability lifecycle event was recorded by the policy layer.
93 CapabilityRecorded(CapabilityLedgerEntry),
94 /// The assistant has requested an interactive user choice.
95 QuestionRequired(QuestionRequest),
96 /// An interactive user choice has been resolved.
97 QuestionResolved(QuestionResponse),
98 /// Plan tool create is waiting for user review (blocks the turn).
99 PlanReviewRequired(PlanReviewRequest),
100 /// User finished plan review.
101 PlanReviewResolved(PlanReviewResponse),
102 /// Sudo password needed (no secret in event payload).
103 SudoPasswordRequired(SudoPasswordRequest),
104 /// A tool invocation has begun execution.
105 ToolStarted(ToolInvocation),
106 /// A tool invocation has completed.
107 ToolCompleted(ToolResult),
108 /// A nested subagent emitted a transient UI activity update.
109 SubagentActivity {
110 /// Parent subagent tool invocation id.
111 invocation_id: String,
112 /// Human-readable description of the latest nested activity.
113 message: String,
114 },
115 /// A nested subagent emitted a transient transcript item for UI drill-down.
116 SubagentTranscript {
117 /// Parent subagent tool invocation id.
118 invocation_id: String,
119 /// Transcript item to append for the active UI session.
120 item: SubagentTranscriptItem,
121 },
122 /// A harness-level diagnostic trace (profile, message count, tool count).
123 HarnessTrace(Value),
124 /// The harness stopped a turn before another model iteration.
125 HarnessStopped {
126 /// Machine-readable stop reason.
127 reason: String,
128 /// Human-readable diagnostic.
129 message: String,
130 /// Tool involved in the stop, when applicable.
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 tool_name: Option<String>,
133 },
134 /// A file patch has been proposed by the assistant.
135 PatchProposed(PatchProposal),
136 /// The conversation context has been updated.
137 ContextUpdated,
138 /// Token usage has been reported by the model provider.
139 TokensUpdated {
140 /// Number of input/prompt tokens consumed.
141 input_tokens: u64,
142 /// Number of output/completion tokens produced.
143 output_tokens: u64,
144 /// Number of tokens written to the prompt cache (Anthropic).
145 cache_creation_tokens: u64,
146 /// Number of tokens read from the prompt cache (Anthropic).
147 cache_read_tokens: u64,
148 },
149 /// The session has been persisted to disk.
150 SessionSaved {
151 /// Identifier of the saved session.
152 session_id: String,
153 },
154 /// Session display title was assigned or updated (provisional or model-named).
155 SessionTitleUpdated {
156 /// Identifier of the session.
157 session_id: String,
158 /// New display title.
159 title: String,
160 },
161 /// A turn has completed with a final text response.
162 TurnCompleted {
163 /// Identifier of the completed turn.
164 turn_id: String,
165 /// Final assistant text for the turn.
166 text: String,
167 },
168 /// The session has ended.
169 SessionFinished {
170 /// Identifier of the finished session.
171 session_id: String,
172 },
173 /// Micro-compaction cleared stale read-only tool results from history.
174 MicroCompactApplied {
175 /// Number of tool result messages that were cleared.
176 messages_cleared: usize,
177 },
178 /// An automatic conversation compaction has started.
179 AutoCompactStarted,
180 /// An automatic conversation compaction has completed.
181 AutoCompactCompleted {
182 /// Estimated number of tokens saved by compaction.
183 tokens_saved: u64,
184 /// Model-produced summary that replaced older turns.
185 #[serde(default)]
186 summary: String,
187 /// Non-system conversation messages retained after the summary.
188 #[serde(default)]
189 kept_recent_messages: usize,
190 },
191 /// An automatic conversation compaction has failed.
192 AutoCompactFailed {
193 /// Human-readable reason for the failure.
194 reason: String,
195 },
196 /// Auto-dream memory consolidation has started.
197 AutoDreamStarted {
198 /// Hours since the last dream run.
199 hours_since_last: u64,
200 /// Number of sessions reviewed.
201 sessions_reviewed: usize,
202 },
203 /// Auto-dream memory consolidation has completed.
204 AutoDreamCompleted {
205 /// Memories marked stale.
206 marked_stale: usize,
207 /// Duplicates merged.
208 duplicates_merged: usize,
209 /// Active memories remaining.
210 active_count: usize,
211 },
212 /// Auto-dream memory consolidation has failed.
213 AutoDreamFailed {
214 /// Human-readable reason for the failure.
215 reason: String,
216 },
217 /// The agent requested to set a goal via natural language.
218 SetGoalRequested {
219 /// The objective text.
220 objective: String,
221 /// Optional short UI label.
222 short_description: Option<String>,
223 /// Optional token budget.
224 token_budget: Option<i64>,
225 },
226 GoalUpdated {
227 /// The session this goal belongs to.
228 session_id: String,
229 /// Unique identifier for the goal.
230 goal_id: String,
231 /// The objective text.
232 objective: String,
233 /// Optional short UI label.
234 short_description: Option<String>,
235 /// Current goal status.
236 status: GoalStatus,
237 /// Tokens consumed so far.
238 tokens_used: i64,
239 /// Optional token budget.
240 token_budget: Option<i64>,
241 },
242 /// An error occurred during agent execution.
243 Error {
244 /// Human-readable error message.
245 message: String,
246 },
247 /// The agent proposed a plan in Plan mode.
248 /// The UI should show a confirmation popup to implement or discard.
249 PlanProposed {
250 /// The session that proposed the plan.
251 session_id: String,
252 /// Title/summary of the plan.
253 title: String,
254 /// Ordered list of steps.
255 steps: Vec<String>,
256 },
257 /// The agent mode changed (e.g. Default → Plan or Plan → Default).
258 AgentModeChanged {
259 /// The session whose mode changed.
260 session_id: String,
261 /// The new mode.
262 mode: crate::plan_mode::AgentMode,
263 },
264 /// Progress from an external ACP agent peer (not a model provider).
265 ///
266 /// Text deltas from ACP are also emitted as [`AssistantDelta`] when
267 /// applicable; this variant carries the full peer update for clients that
268 /// want tool/plan/permission detail from the external agent.
269 AcpPeerUpdate {
270 /// Configured ACP agent id (e.g. `"devin"`).
271 agent_id: String,
272 /// ACP session id on the external agent.
273 acp_session_id: String,
274 /// Discriminator for the update kind (`agent_message_chunk`, `tool_call`, …).
275 update_kind: String,
276 /// Full update payload as JSON.
277 update: Value,
278 },
279}
280
281impl RuntimeEventKind {
282 /// Converts this event kind into an [`AgentEvent`] if applicable.
283 ///
284 /// Returns `None` for lifecycle-only events that have no direct
285 /// agent-level counterpart (session/turn lifecycle, tool started,
286 /// context updated).
287 pub fn into_agent_event(self) -> Option<AgentEvent> {
288 match self {
289 RuntimeEventKind::AssistantDelta { text } => Some(AgentEvent::ModelDelta { text }),
290 RuntimeEventKind::AssistantThinkingDelta { text } => {
291 Some(AgentEvent::ModelThinkingDelta { text })
292 }
293 RuntimeEventKind::ToolRequested(invocation) => {
294 Some(AgentEvent::ToolRequested(invocation))
295 }
296 RuntimeEventKind::ApprovalRequired(request) => {
297 Some(AgentEvent::ApprovalRequested(request))
298 }
299 RuntimeEventKind::ApprovalResolved(decision) => {
300 Some(AgentEvent::ApprovalResolved(decision))
301 }
302 RuntimeEventKind::CapabilityRecorded(entry) => {
303 Some(AgentEvent::CapabilityRecorded(entry))
304 }
305 RuntimeEventKind::QuestionRequired(request) => {
306 Some(AgentEvent::QuestionRequested(request))
307 }
308 RuntimeEventKind::QuestionResolved(response) => {
309 Some(AgentEvent::QuestionResolved(response))
310 }
311 RuntimeEventKind::PlanReviewRequired(request) => {
312 Some(AgentEvent::PlanReviewRequested(request))
313 }
314 RuntimeEventKind::PlanReviewResolved(response) => {
315 Some(AgentEvent::PlanReviewResolved(response))
316 }
317 RuntimeEventKind::SudoPasswordRequired(request) => {
318 Some(AgentEvent::SudoPasswordRequested(request))
319 }
320 RuntimeEventKind::ToolCompleted(result) => Some(AgentEvent::ToolCompleted(result)),
321 RuntimeEventKind::SubagentActivity {
322 invocation_id,
323 message,
324 } => Some(AgentEvent::SubagentActivity {
325 invocation_id,
326 message,
327 }),
328 RuntimeEventKind::SubagentTranscript {
329 invocation_id,
330 item,
331 } => Some(AgentEvent::SubagentTranscript {
332 invocation_id,
333 item,
334 }),
335 RuntimeEventKind::HarnessTrace(value) => Some(AgentEvent::HarnessTrace(value)),
336 RuntimeEventKind::HarnessStopped {
337 reason,
338 message,
339 tool_name,
340 } => Some(AgentEvent::HarnessStopped {
341 reason,
342 message,
343 tool_name,
344 }),
345 RuntimeEventKind::PatchProposed(patch) => Some(AgentEvent::PatchProposed(patch)),
346 RuntimeEventKind::TokensUpdated {
347 input_tokens,
348 output_tokens,
349 cache_creation_tokens,
350 cache_read_tokens,
351 } => Some(AgentEvent::UsageReported {
352 input_tokens,
353 output_tokens,
354 cache_creation_tokens,
355 cache_read_tokens,
356 }),
357 RuntimeEventKind::MicroCompactApplied { messages_cleared } => {
358 Some(AgentEvent::MicroCompactApplied { messages_cleared })
359 }
360 RuntimeEventKind::AutoCompactStarted => Some(AgentEvent::AutoCompactStarted),
361 RuntimeEventKind::AutoCompactCompleted {
362 tokens_saved,
363 summary,
364 kept_recent_messages,
365 } => Some(AgentEvent::AutoCompactCompleted {
366 tokens_saved,
367 summary,
368 kept_recent_messages,
369 }),
370 RuntimeEventKind::AutoCompactFailed { reason } => {
371 Some(AgentEvent::AutoCompactFailed { reason })
372 }
373 RuntimeEventKind::Error { message } => Some(AgentEvent::Error { message }),
374 RuntimeEventKind::SetGoalRequested {
375 objective,
376 short_description,
377 token_budget,
378 } => Some(AgentEvent::SetGoalRequested {
379 objective,
380 short_description,
381 token_budget,
382 }),
383 RuntimeEventKind::GoalUpdated {
384 session_id,
385 goal_id,
386 objective,
387 short_description,
388 status,
389 tokens_used,
390 token_budget,
391 } => Some(AgentEvent::GoalUpdated {
392 session_id,
393 goal_id,
394 objective,
395 short_description,
396 status,
397 tokens_used,
398 token_budget,
399 }),
400 RuntimeEventKind::PlanProposed { title, steps, .. } => {
401 Some(AgentEvent::PlanProposed { title, steps })
402 }
403 RuntimeEventKind::AgentModeChanged { mode, .. } => {
404 Some(AgentEvent::AgentModeChanged { mode })
405 }
406 _ => None,
407 }
408 }
409}
410
411/// A high-level agent event suitable for client consumption.
412///
413/// Unlike [`RuntimeEventKind`], agent events represent the semantic actions
414/// a client cares about: user input, model output, tool calls, approvals,
415/// compaction, usage, and errors.
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub enum AgentEvent {
418 /// The user submitted a new task or message.
419 UserTaskSubmitted {
420 /// The user's input text.
421 text: String,
422 /// Optional multimodal content parts (images + text).
423 #[serde(default, skip_serializing_if = "Vec::is_empty")]
424 content_parts: Vec<crate::model::ContentPart>,
425 /// Unix timestamp (seconds since epoch) when the user submitted this
426 /// message. Used for wall-clock display in clients (e.g. TUI sticky bar).
427 /// Optional for backward-compatible session JSON (pre-timestamp sessions).
428 #[serde(default, skip_serializing_if = "Option::is_none")]
429 submitted_at: Option<u64>,
430 },
431 /// A complete model output with optional thinking/reasoning content.
432 ModelOutput {
433 /// The assistant's response text.
434 text: String,
435 /// Optional thinking or reasoning trace from the model.
436 #[serde(default)]
437 thinking: Option<String>,
438 },
439 /// A streaming text delta from the model.
440 ModelDelta {
441 /// Incremental text content.
442 text: String,
443 },
444 /// A streaming thinking/reasoning delta from the model.
445 ModelThinkingDelta {
446 /// Incremental thinking content.
447 text: String,
448 },
449 /// Live-only: the model is streaming tool-call arguments before
450 /// [`ToolRequested`]. Not persisted in the session event log.
451 ToolCallStreaming {
452 /// Provider tool-call id when known.
453 #[serde(default, skip_serializing_if = "Option::is_none")]
454 id: Option<String>,
455 /// Tool name when known.
456 tool_name: String,
457 /// Total characters of arguments streamed so far for this call.
458 #[serde(default)]
459 arguments_chars: usize,
460 },
461 /// The assistant requested a tool invocation.
462 ToolRequested(ToolInvocation),
463 /// A tool invocation completed.
464 ToolCompleted(ToolResult),
465 /// Transient status for a nested subagent.
466 SubagentActivity {
467 /// Parent subagent tool invocation id.
468 invocation_id: String,
469 /// Human-readable description of the latest nested activity.
470 message: String,
471 },
472 /// Transient drill-down transcript item for a nested subagent.
473 SubagentTranscript {
474 /// Parent subagent tool invocation id.
475 invocation_id: String,
476 /// Transcript item to append for this UI session.
477 item: SubagentTranscriptItem,
478 },
479 /// A harness-level diagnostic trace.
480 HarnessTrace(Value),
481 /// The harness stopped a turn before another model iteration.
482 HarnessStopped {
483 /// Machine-readable stop reason.
484 reason: String,
485 /// Human-readable diagnostic.
486 message: String,
487 /// Tool involved in the stop, when applicable.
488 #[serde(default, skip_serializing_if = "Option::is_none")]
489 tool_name: Option<String>,
490 },
491 /// A file patch was proposed by the assistant.
492 PatchProposed(PatchProposal),
493 /// A tool invocation requires user approval.
494 ApprovalRequested(ApprovalRequest),
495 /// An approval request was resolved.
496 ApprovalResolved(ApprovalDecision),
497 /// A capability lifecycle event was recorded by the policy layer.
498 CapabilityRecorded(CapabilityLedgerEntry),
499 /// The assistant requested an interactive user choice.
500 QuestionRequested(QuestionRequest),
501 /// An interactive user choice was resolved.
502 QuestionResolved(QuestionResponse),
503 /// Plan tool create is waiting for user review (blocks the turn).
504 PlanReviewRequested(PlanReviewRequest),
505 /// User finished plan review (approve / changes / quit).
506 PlanReviewResolved(PlanReviewResponse),
507 /// Bash/`sudo` needs a password; TUI shows a masked modal. Password is
508 /// never included in this event — only a correlation id. The secret is
509 /// delivered solely through the sudo password resolver oneshot.
510 SudoPasswordRequested(SudoPasswordRequest),
511 /// The same tool was called consecutively with identical arguments.
512 /// The tool is NOT executed; this is a notification to the user.
513 RepeatedToolCallWarning {
514 /// Name of the repeated tool.
515 tool_name: String,
516 /// Warning message describing the repetition.
517 message: String,
518 },
519 /// Repetitive/degenerate model output was detected (character runs,
520 /// alternating patterns, or duplicate thinking blocks).
521 RepetitionDetected {
522 /// What kind of repetition was detected.
523 kind: RepetitionWarningKind,
524 /// Human-readable warning message.
525 message: String,
526 },
527 /// An error occurred.
528 Error {
529 /// Human-readable error message.
530 message: String,
531 },
532 /// Auto-dream memory consolidation has started.
533 AutoDreamStarted {
534 /// Hours since the last dream run.
535 hours_since_last: u64,
536 /// Number of sessions reviewed.
537 sessions_reviewed: usize,
538 },
539 /// Auto-dream memory consolidation has completed.
540 AutoDreamCompleted {
541 /// Memories marked stale.
542 marked_stale: usize,
543 /// Duplicates merged.
544 duplicates_merged: usize,
545 /// Active memories remaining.
546 active_count: usize,
547 },
548 /// Auto-dream memory consolidation has failed.
549 AutoDreamFailed {
550 /// Human-readable reason for the failure.
551 reason: String,
552 },
553 /// Token usage was reported by the model provider.
554 UsageReported {
555 /// Number of input/prompt tokens consumed.
556 input_tokens: u64,
557 /// Number of output/completion tokens produced.
558 output_tokens: u64,
559 /// Number of tokens written to the prompt cache (Anthropic).
560 cache_creation_tokens: u64,
561 /// Number of tokens read from the prompt cache (Anthropic).
562 cache_read_tokens: u64,
563 },
564 /// Short post-turn session recap ("Recap" line).
565 SessionRecap {
566 /// One- or two-sentence summary of the turn.
567 summary: String,
568 /// When true, the recap was generated but should not be shown (long-tail).
569 suppressed: bool,
570 },
571 /// The provider stream broke mid-generation and is being resumed via
572 /// prefill (assistant continuation). The UI can show a transient hint.
573 StreamResuming {
574 /// Characters of text accumulated before the break.
575 accumulated_chars: usize,
576 /// Retry attempt number (1-based).
577 attempt: u32,
578 },
579 /// The agent requested to set a goal via natural language.
580 SetGoalRequested {
581 /// The objective text.
582 objective: String,
583 /// Optional short UI label.
584 short_description: Option<String>,
585 /// Optional token budget.
586 token_budget: Option<i64>,
587 },
588 /// The session goal was updated (created, status change, budget exceeded).
589 GoalUpdated {
590 /// The session this goal belongs to.
591 session_id: String,
592 /// Unique identifier for the goal.
593 goal_id: String,
594 /// The objective text.
595 objective: String,
596 /// Optional short UI label.
597 short_description: Option<String>,
598 /// Current goal status.
599 status: GoalStatus,
600 /// Tokens consumed so far.
601 tokens_used: i64,
602 /// Optional token budget.
603 token_budget: Option<i64>,
604 },
605 /// Micro-compaction cleared stale tool results from history.
606 MicroCompactApplied {
607 /// Number of tool result messages cleared.
608 messages_cleared: usize,
609 },
610 /// Automatic conversation compaction started.
611 AutoCompactStarted,
612 /// Automatic conversation compaction completed.
613 AutoCompactCompleted {
614 /// Estimated tokens saved by compaction.
615 tokens_saved: u64,
616 /// Model-produced summary that replaced older turns.
617 #[serde(default)]
618 summary: String,
619 /// Non-system conversation messages retained after the summary.
620 #[serde(default)]
621 kept_recent_messages: usize,
622 },
623 /// Automatic conversation compaction failed.
624 AutoCompactFailed {
625 /// Human-readable failure reason.
626 reason: String,
627 },
628 /// The agent proposed a plan in Plan mode.
629 PlanProposed {
630 /// Title/summary of the plan.
631 title: String,
632 /// Ordered list of steps.
633 steps: Vec<String>,
634 },
635 /// The agent mode changed (e.g. Default → Plan or Plan → Default).
636 AgentModeChanged {
637 /// The new mode.
638 mode: crate::plan_mode::AgentMode,
639 },
640 /// Host should surface a user-visible notification.
641 ///
642 /// Desktop hosts call OS toasts; browser hosts map this to the Web
643 /// Notifications API. Payload mirrors [`crate::notify::NotifyRequest`].
644 NotificationRequested {
645 title: String,
646 body: String,
647 #[serde(default)]
648 urgency: crate::notify::NotificationUrgency,
649 #[serde(default, skip_serializing_if = "Option::is_none")]
650 category: Option<String>,
651 },
652 /// A newer NAVI release is available (from update check).
653 UpdateAvailable {
654 current_version: String,
655 latest_version: String,
656 latest_tag: String,
657 release_url: String,
658 #[serde(default, skip_serializing_if = "Option::is_none")]
659 body: Option<String>,
660 #[serde(default)]
661 prerelease: bool,
662 },
663}
664
665/// Kind of repetitive/degenerate output detected by the repetition detector.
666#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
667#[serde(tag = "type")]
668pub enum RepetitionWarningKind {
669 /// Same character repeated many times (e.g. "aaaaaa...").
670 CharRun {
671 /// The repeating character.
672 ch: char,
673 /// How many consecutive occurrences.
674 count: usize,
675 },
676 /// Two characters alternating many times (e.g. "-_-_-_").
677 AlternatingPattern {
678 /// The two-character pattern (e.g. "-_").
679 pattern: String,
680 /// How many cycles detected.
681 cycles: usize,
682 },
683}
684
685/// A pending approval request for a tool invocation that requires user consent.
686#[derive(Debug, Clone, Serialize, Deserialize)]
687pub struct ApprovalRequest {
688 /// Unique identifier for this approval request.
689 pub id: String,
690 /// Human-readable summary of what the tool will do.
691 pub summary: String,
692 /// The security risk category that triggered the approval requirement.
693 pub risk: ApprovalRisk,
694}
695
696/// A selectable option in a [`QuestionRequest`].
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698pub struct QuestionOption {
699 /// Short option label shown in the selection UI and returned to the model.
700 pub label: String,
701 /// Optional explanatory text shown below the label.
702 #[serde(default, skip_serializing_if = "Option::is_none")]
703 pub description: Option<String>,
704}
705
706/// A pending interactive question requested by the assistant through the
707/// `question` tool.
708#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
709pub struct QuestionRequest {
710 /// Unique identifier matching the tool invocation id.
711 pub id: String,
712 /// Prompt shown to the user.
713 pub question: String,
714 /// Selectable options.
715 #[serde(default)]
716 pub options: Vec<QuestionOption>,
717 /// Whether more than one option may be selected.
718 #[serde(default)]
719 pub multiple: bool,
720 /// Whether the UI should allow a free-form custom answer.
721 #[serde(default)]
722 pub allow_custom: bool,
723}
724
725/// Resolution for an interactive [`QuestionRequest`].
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
727#[serde(tag = "kind", rename_all = "snake_case")]
728pub enum QuestionResponse {
729 /// The user selected one or more answers.
730 Answered {
731 /// Question/tool invocation id.
732 id: String,
733 /// Selected labels or the custom answer text.
734 answers: Vec<String>,
735 },
736 /// The user dismissed the question without answering.
737 Dismissed {
738 /// Question/tool invocation id.
739 id: String,
740 },
741}
742
743impl QuestionResponse {
744 /// Returns the request id this response resolves.
745 pub fn id(&self) -> &str {
746 match self {
747 Self::Answered { id, .. } | Self::Dismissed { id } => id,
748 }
749 }
750}
751
752/// Interactive plan review requested after `plan(action=submit|create)`.
753/// The turn **blocks** until the user resolves it.
754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
755pub struct PlanReviewRequest {
756 /// Tool invocation id (used as correlation key for the oneshot).
757 pub id: String,
758 /// Persisted plan id in SQLite.
759 pub plan_id: String,
760 pub title: String,
761 pub description: String,
762 pub steps: Vec<String>,
763 /// Full markdown design doc (primary content for review UI).
764 #[serde(default)]
765 pub body_markdown: String,
766 /// On-disk plan markdown file path.
767 #[serde(default)]
768 pub plan_file_path: String,
769}
770
771/// User decision from the plan review modal.
772#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
773#[serde(rename_all = "snake_case")]
774pub enum PlanReviewDecision {
775 Approve,
776 RequestChanges,
777 Quit,
778}
779
780/// Resolution for a blocked plan review.
781#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
782pub struct PlanReviewResponse {
783 /// Matching tool invocation id.
784 pub id: String,
785 pub plan_id: String,
786 pub decision: PlanReviewDecision,
787 /// Line-oriented comments from the modal.
788 #[serde(default)]
789 pub comments: Vec<crate::plan_store::PlanLineComment>,
790 /// Freeform notes from the prompt field.
791 #[serde(default)]
792 pub freeform: String,
793}
794
795impl PlanReviewResponse {
796 pub fn id(&self) -> &str {
797 &self.id
798 }
799}
800
801/// Request for a sudo password from the interactive TUI.
802///
803/// **Security:** this event must never carry the password itself — only a
804/// correlation id and UI context (command summary). The secret is delivered
805/// solely through [`crate::runtime::SudoPasswordResolver`].
806#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
807pub struct SudoPasswordRequest {
808 /// Correlation id for the oneshot resolver.
809 pub id: String,
810 /// Short, non-secret description (e.g. truncated command).
811 pub command_summary: String,
812}
813
814/// Resolution of a sudo password prompt.
815///
816/// Prefer not to serialize responses that still hold a password into logs.
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818#[serde(tag = "kind", rename_all = "snake_case")]
819pub enum SudoPasswordResponse {
820 /// User entered a password. Cleared from memory after bash consumes it.
821 Submitted {
822 id: String,
823 /// Secret — never write to chat history or tool observations.
824 #[serde(skip_serializing)]
825 password: String,
826 },
827 /// User cancelled the modal.
828 Cancelled { id: String },
829}
830
831impl SudoPasswordResponse {
832 pub fn id(&self) -> &str {
833 match self {
834 Self::Submitted { id, .. } | Self::Cancelled { id } => id,
835 }
836 }
837}
838
839/// The security risk category associated with an approval request.
840#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
841pub enum ApprovalRisk {
842 /// Any tool execution in restricted mode.
843 Tool,
844 /// A file write operation.
845 Write,
846 /// A shell command execution.
847 Command,
848 /// A guarded command that requires explicit approval outside YOLO mode.
849 Guarded,
850 /// Loading or executing an external plugin.
851 ExternalPlugin,
852}
853
854/// The outcome of an approval request.
855#[derive(Debug, Clone, Serialize, Deserialize)]
856pub enum ApprovalDecision {
857 /// The user approved the action.
858 Approved {
859 /// Identifier matching the [`ApprovalRequest::id`].
860 id: String,
861 },
862 /// The user denied the action.
863 Denied {
864 /// Identifier matching the [`ApprovalRequest::id`].
865 id: String,
866 },
867}