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 },
185 /// An automatic conversation compaction has failed.
186 AutoCompactFailed {
187 /// Human-readable reason for the failure.
188 reason: String,
189 },
190 /// Auto-dream memory consolidation has started.
191 AutoDreamStarted {
192 /// Hours since the last dream run.
193 hours_since_last: u64,
194 /// Number of sessions reviewed.
195 sessions_reviewed: usize,
196 },
197 /// Auto-dream memory consolidation has completed.
198 AutoDreamCompleted {
199 /// Memories marked stale.
200 marked_stale: usize,
201 /// Duplicates merged.
202 duplicates_merged: usize,
203 /// Active memories remaining.
204 active_count: usize,
205 },
206 /// Auto-dream memory consolidation has failed.
207 AutoDreamFailed {
208 /// Human-readable reason for the failure.
209 reason: String,
210 },
211 /// The agent requested to set a goal via natural language.
212 SetGoalRequested {
213 /// The objective text.
214 objective: String,
215 /// Optional short UI label.
216 short_description: Option<String>,
217 /// Optional token budget.
218 token_budget: Option<i64>,
219 },
220 GoalUpdated {
221 /// The session this goal belongs to.
222 session_id: String,
223 /// Unique identifier for the goal.
224 goal_id: String,
225 /// The objective text.
226 objective: String,
227 /// Optional short UI label.
228 short_description: Option<String>,
229 /// Current goal status.
230 status: GoalStatus,
231 /// Tokens consumed so far.
232 tokens_used: i64,
233 /// Optional token budget.
234 token_budget: Option<i64>,
235 },
236 /// An error occurred during agent execution.
237 Error {
238 /// Human-readable error message.
239 message: String,
240 },
241 /// The agent proposed a plan in Plan mode.
242 /// The UI should show a confirmation popup to implement or discard.
243 PlanProposed {
244 /// The session that proposed the plan.
245 session_id: String,
246 /// Title/summary of the plan.
247 title: String,
248 /// Ordered list of steps.
249 steps: Vec<String>,
250 },
251 /// The agent mode changed (e.g. Default → Plan or Plan → Default).
252 AgentModeChanged {
253 /// The session whose mode changed.
254 session_id: String,
255 /// The new mode.
256 mode: crate::plan_mode::AgentMode,
257 },
258}
259
260impl RuntimeEventKind {
261 /// Converts this event kind into an [`AgentEvent`] if applicable.
262 ///
263 /// Returns `None` for lifecycle-only events that have no direct
264 /// agent-level counterpart (session/turn lifecycle, tool started,
265 /// context updated).
266 pub fn into_agent_event(self) -> Option<AgentEvent> {
267 match self {
268 RuntimeEventKind::AssistantDelta { text } => Some(AgentEvent::ModelDelta { text }),
269 RuntimeEventKind::AssistantThinkingDelta { text } => {
270 Some(AgentEvent::ModelThinkingDelta { text })
271 }
272 RuntimeEventKind::ToolRequested(invocation) => {
273 Some(AgentEvent::ToolRequested(invocation))
274 }
275 RuntimeEventKind::ApprovalRequired(request) => {
276 Some(AgentEvent::ApprovalRequested(request))
277 }
278 RuntimeEventKind::ApprovalResolved(decision) => {
279 Some(AgentEvent::ApprovalResolved(decision))
280 }
281 RuntimeEventKind::CapabilityRecorded(entry) => {
282 Some(AgentEvent::CapabilityRecorded(entry))
283 }
284 RuntimeEventKind::QuestionRequired(request) => {
285 Some(AgentEvent::QuestionRequested(request))
286 }
287 RuntimeEventKind::QuestionResolved(response) => {
288 Some(AgentEvent::QuestionResolved(response))
289 }
290 RuntimeEventKind::PlanReviewRequired(request) => {
291 Some(AgentEvent::PlanReviewRequested(request))
292 }
293 RuntimeEventKind::PlanReviewResolved(response) => {
294 Some(AgentEvent::PlanReviewResolved(response))
295 }
296 RuntimeEventKind::SudoPasswordRequired(request) => {
297 Some(AgentEvent::SudoPasswordRequested(request))
298 }
299 RuntimeEventKind::ToolCompleted(result) => Some(AgentEvent::ToolCompleted(result)),
300 RuntimeEventKind::SubagentActivity {
301 invocation_id,
302 message,
303 } => Some(AgentEvent::SubagentActivity {
304 invocation_id,
305 message,
306 }),
307 RuntimeEventKind::SubagentTranscript {
308 invocation_id,
309 item,
310 } => Some(AgentEvent::SubagentTranscript {
311 invocation_id,
312 item,
313 }),
314 RuntimeEventKind::HarnessTrace(value) => Some(AgentEvent::HarnessTrace(value)),
315 RuntimeEventKind::HarnessStopped {
316 reason,
317 message,
318 tool_name,
319 } => Some(AgentEvent::HarnessStopped {
320 reason,
321 message,
322 tool_name,
323 }),
324 RuntimeEventKind::PatchProposed(patch) => Some(AgentEvent::PatchProposed(patch)),
325 RuntimeEventKind::TokensUpdated {
326 input_tokens,
327 output_tokens,
328 cache_creation_tokens,
329 cache_read_tokens,
330 } => Some(AgentEvent::UsageReported {
331 input_tokens,
332 output_tokens,
333 cache_creation_tokens,
334 cache_read_tokens,
335 }),
336 RuntimeEventKind::MicroCompactApplied { messages_cleared } => {
337 Some(AgentEvent::MicroCompactApplied { messages_cleared })
338 }
339 RuntimeEventKind::AutoCompactStarted => Some(AgentEvent::AutoCompactStarted),
340 RuntimeEventKind::AutoCompactCompleted { tokens_saved } => {
341 Some(AgentEvent::AutoCompactCompleted { tokens_saved })
342 }
343 RuntimeEventKind::AutoCompactFailed { reason } => {
344 Some(AgentEvent::AutoCompactFailed { reason })
345 }
346 RuntimeEventKind::Error { message } => Some(AgentEvent::Error { message }),
347 RuntimeEventKind::SetGoalRequested {
348 objective,
349 short_description,
350 token_budget,
351 } => Some(AgentEvent::SetGoalRequested {
352 objective,
353 short_description,
354 token_budget,
355 }),
356 RuntimeEventKind::GoalUpdated {
357 session_id,
358 goal_id,
359 objective,
360 short_description,
361 status,
362 tokens_used,
363 token_budget,
364 } => Some(AgentEvent::GoalUpdated {
365 session_id,
366 goal_id,
367 objective,
368 short_description,
369 status,
370 tokens_used,
371 token_budget,
372 }),
373 RuntimeEventKind::PlanProposed { title, steps, .. } => {
374 Some(AgentEvent::PlanProposed { title, steps })
375 }
376 RuntimeEventKind::AgentModeChanged { mode, .. } => {
377 Some(AgentEvent::AgentModeChanged { mode })
378 }
379 _ => None,
380 }
381 }
382}
383
384/// A high-level agent event suitable for client consumption.
385///
386/// Unlike [`RuntimeEventKind`], agent events represent the semantic actions
387/// a client cares about: user input, model output, tool calls, approvals,
388/// compaction, usage, and errors.
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub enum AgentEvent {
391 /// The user submitted a new task or message.
392 UserTaskSubmitted {
393 /// The user's input text.
394 text: String,
395 /// Optional multimodal content parts (images + text).
396 #[serde(default, skip_serializing_if = "Vec::is_empty")]
397 content_parts: Vec<crate::model::ContentPart>,
398 /// Unix timestamp (seconds since epoch) when the user submitted this
399 /// message. Used for wall-clock display in clients (e.g. TUI sticky bar).
400 /// Optional for backward-compatible session JSON (pre-timestamp sessions).
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 submitted_at: Option<u64>,
403 },
404 /// A complete model output with optional thinking/reasoning content.
405 ModelOutput {
406 /// The assistant's response text.
407 text: String,
408 /// Optional thinking or reasoning trace from the model.
409 #[serde(default)]
410 thinking: Option<String>,
411 },
412 /// A streaming text delta from the model.
413 ModelDelta {
414 /// Incremental text content.
415 text: String,
416 },
417 /// A streaming thinking/reasoning delta from the model.
418 ModelThinkingDelta {
419 /// Incremental thinking content.
420 text: String,
421 },
422 /// The assistant requested a tool invocation.
423 ToolRequested(ToolInvocation),
424 /// A tool invocation completed.
425 ToolCompleted(ToolResult),
426 /// Transient status for a nested subagent.
427 SubagentActivity {
428 /// Parent subagent tool invocation id.
429 invocation_id: String,
430 /// Human-readable description of the latest nested activity.
431 message: String,
432 },
433 /// Transient drill-down transcript item for a nested subagent.
434 SubagentTranscript {
435 /// Parent subagent tool invocation id.
436 invocation_id: String,
437 /// Transcript item to append for this UI session.
438 item: SubagentTranscriptItem,
439 },
440 /// A harness-level diagnostic trace.
441 HarnessTrace(Value),
442 /// The harness stopped a turn before another model iteration.
443 HarnessStopped {
444 /// Machine-readable stop reason.
445 reason: String,
446 /// Human-readable diagnostic.
447 message: String,
448 /// Tool involved in the stop, when applicable.
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 tool_name: Option<String>,
451 },
452 /// A file patch was proposed by the assistant.
453 PatchProposed(PatchProposal),
454 /// A tool invocation requires user approval.
455 ApprovalRequested(ApprovalRequest),
456 /// An approval request was resolved.
457 ApprovalResolved(ApprovalDecision),
458 /// A capability lifecycle event was recorded by the policy layer.
459 CapabilityRecorded(CapabilityLedgerEntry),
460 /// The assistant requested an interactive user choice.
461 QuestionRequested(QuestionRequest),
462 /// An interactive user choice was resolved.
463 QuestionResolved(QuestionResponse),
464 /// Plan tool create is waiting for user review (blocks the turn).
465 PlanReviewRequested(PlanReviewRequest),
466 /// User finished plan review (approve / changes / quit).
467 PlanReviewResolved(PlanReviewResponse),
468 /// Bash/`sudo` needs a password; TUI shows a masked modal. Password is
469 /// never included in this event — only a correlation id. The secret is
470 /// delivered solely through the sudo password resolver oneshot.
471 SudoPasswordRequested(SudoPasswordRequest),
472 /// The same tool was called consecutively with identical arguments.
473 /// The tool is NOT executed; this is a notification to the user.
474 RepeatedToolCallWarning {
475 /// Name of the repeated tool.
476 tool_name: String,
477 /// Warning message describing the repetition.
478 message: String,
479 },
480 /// Repetitive/degenerate model output was detected (character runs,
481 /// alternating patterns, or duplicate thinking blocks).
482 RepetitionDetected {
483 /// What kind of repetition was detected.
484 kind: RepetitionWarningKind,
485 /// Human-readable warning message.
486 message: String,
487 },
488 /// An error occurred.
489 Error {
490 /// Human-readable error message.
491 message: String,
492 },
493 /// Auto-dream memory consolidation has started.
494 AutoDreamStarted {
495 /// Hours since the last dream run.
496 hours_since_last: u64,
497 /// Number of sessions reviewed.
498 sessions_reviewed: usize,
499 },
500 /// Auto-dream memory consolidation has completed.
501 AutoDreamCompleted {
502 /// Memories marked stale.
503 marked_stale: usize,
504 /// Duplicates merged.
505 duplicates_merged: usize,
506 /// Active memories remaining.
507 active_count: usize,
508 },
509 /// Auto-dream memory consolidation has failed.
510 AutoDreamFailed {
511 /// Human-readable reason for the failure.
512 reason: String,
513 },
514 /// Token usage was reported by the model provider.
515 UsageReported {
516 /// Number of input/prompt tokens consumed.
517 input_tokens: u64,
518 /// Number of output/completion tokens produced.
519 output_tokens: u64,
520 /// Number of tokens written to the prompt cache (Anthropic).
521 cache_creation_tokens: u64,
522 /// Number of tokens read from the prompt cache (Anthropic).
523 cache_read_tokens: u64,
524 },
525 /// Short post-turn session recap ("Recap" line).
526 SessionRecap {
527 /// One- or two-sentence summary of the turn.
528 summary: String,
529 /// When true, the recap was generated but should not be shown (long-tail).
530 suppressed: bool,
531 },
532 /// The provider stream broke mid-generation and is being resumed via
533 /// prefill (assistant continuation). The UI can show a transient hint.
534 StreamResuming {
535 /// Characters of text accumulated before the break.
536 accumulated_chars: usize,
537 /// Retry attempt number (1-based).
538 attempt: u32,
539 },
540 /// The agent requested to set a goal via natural language.
541 SetGoalRequested {
542 /// The objective text.
543 objective: String,
544 /// Optional short UI label.
545 short_description: Option<String>,
546 /// Optional token budget.
547 token_budget: Option<i64>,
548 },
549 /// The session goal was updated (created, status change, budget exceeded).
550 GoalUpdated {
551 /// The session this goal belongs to.
552 session_id: String,
553 /// Unique identifier for the goal.
554 goal_id: String,
555 /// The objective text.
556 objective: String,
557 /// Optional short UI label.
558 short_description: Option<String>,
559 /// Current goal status.
560 status: GoalStatus,
561 /// Tokens consumed so far.
562 tokens_used: i64,
563 /// Optional token budget.
564 token_budget: Option<i64>,
565 },
566 /// Micro-compaction cleared stale tool results from history.
567 MicroCompactApplied {
568 /// Number of tool result messages cleared.
569 messages_cleared: usize,
570 },
571 /// Automatic conversation compaction started.
572 AutoCompactStarted,
573 /// Automatic conversation compaction completed.
574 AutoCompactCompleted {
575 /// Estimated tokens saved by compaction.
576 tokens_saved: u64,
577 },
578 /// Automatic conversation compaction failed.
579 AutoCompactFailed {
580 /// Human-readable failure reason.
581 reason: String,
582 },
583 /// The agent proposed a plan in Plan mode.
584 PlanProposed {
585 /// Title/summary of the plan.
586 title: String,
587 /// Ordered list of steps.
588 steps: Vec<String>,
589 },
590 /// The agent mode changed (e.g. Default → Plan or Plan → Default).
591 AgentModeChanged {
592 /// The new mode.
593 mode: crate::plan_mode::AgentMode,
594 },
595 /// Host should surface a user-visible notification.
596 ///
597 /// Desktop hosts call OS toasts; browser hosts map this to the Web
598 /// Notifications API. Payload mirrors [`crate::notify::NotifyRequest`].
599 NotificationRequested {
600 title: String,
601 body: String,
602 #[serde(default)]
603 urgency: crate::notify::NotificationUrgency,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
605 category: Option<String>,
606 },
607 /// A newer NAVI release is available (from update check).
608 UpdateAvailable {
609 current_version: String,
610 latest_version: String,
611 latest_tag: String,
612 release_url: String,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
614 body: Option<String>,
615 #[serde(default)]
616 prerelease: bool,
617 },
618}
619
620/// Kind of repetitive/degenerate output detected by the repetition detector.
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
622#[serde(tag = "type")]
623pub enum RepetitionWarningKind {
624 /// Same character repeated many times (e.g. "aaaaaa...").
625 CharRun {
626 /// The repeating character.
627 ch: char,
628 /// How many consecutive occurrences.
629 count: usize,
630 },
631 /// Two characters alternating many times (e.g. "-_-_-_").
632 AlternatingPattern {
633 /// The two-character pattern (e.g. "-_").
634 pattern: String,
635 /// How many cycles detected.
636 cycles: usize,
637 },
638}
639
640/// A pending approval request for a tool invocation that requires user consent.
641#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct ApprovalRequest {
643 /// Unique identifier for this approval request.
644 pub id: String,
645 /// Human-readable summary of what the tool will do.
646 pub summary: String,
647 /// The security risk category that triggered the approval requirement.
648 pub risk: ApprovalRisk,
649}
650
651/// A selectable option in a [`QuestionRequest`].
652#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
653pub struct QuestionOption {
654 /// Short option label shown in the selection UI and returned to the model.
655 pub label: String,
656 /// Optional explanatory text shown below the label.
657 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub description: Option<String>,
659}
660
661/// A pending interactive question requested by the assistant through the
662/// `question` tool.
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
664pub struct QuestionRequest {
665 /// Unique identifier matching the tool invocation id.
666 pub id: String,
667 /// Prompt shown to the user.
668 pub question: String,
669 /// Selectable options.
670 #[serde(default)]
671 pub options: Vec<QuestionOption>,
672 /// Whether more than one option may be selected.
673 #[serde(default)]
674 pub multiple: bool,
675 /// Whether the UI should allow a free-form custom answer.
676 #[serde(default)]
677 pub allow_custom: bool,
678}
679
680/// Resolution for an interactive [`QuestionRequest`].
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
682#[serde(tag = "kind", rename_all = "snake_case")]
683pub enum QuestionResponse {
684 /// The user selected one or more answers.
685 Answered {
686 /// Question/tool invocation id.
687 id: String,
688 /// Selected labels or the custom answer text.
689 answers: Vec<String>,
690 },
691 /// The user dismissed the question without answering.
692 Dismissed {
693 /// Question/tool invocation id.
694 id: String,
695 },
696}
697
698impl QuestionResponse {
699 /// Returns the request id this response resolves.
700 pub fn id(&self) -> &str {
701 match self {
702 Self::Answered { id, .. } | Self::Dismissed { id } => id,
703 }
704 }
705}
706
707/// Interactive plan review requested after `plan(action=create)`.
708/// The turn **blocks** until the user resolves it.
709#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
710pub struct PlanReviewRequest {
711 /// Tool invocation id (used as correlation key for the oneshot).
712 pub id: String,
713 /// Persisted plan id in SQLite.
714 pub plan_id: String,
715 pub title: String,
716 pub description: String,
717 pub steps: Vec<String>,
718}
719
720/// User decision from the plan review modal.
721#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
722#[serde(rename_all = "snake_case")]
723pub enum PlanReviewDecision {
724 Approve,
725 RequestChanges,
726 Quit,
727}
728
729/// Resolution for a blocked plan review.
730#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
731pub struct PlanReviewResponse {
732 /// Matching tool invocation id.
733 pub id: String,
734 pub plan_id: String,
735 pub decision: PlanReviewDecision,
736 /// Line-oriented comments from the modal.
737 #[serde(default)]
738 pub comments: Vec<crate::plan_store::PlanLineComment>,
739 /// Freeform notes from the prompt field.
740 #[serde(default)]
741 pub freeform: String,
742}
743
744impl PlanReviewResponse {
745 pub fn id(&self) -> &str {
746 &self.id
747 }
748}
749
750/// Request for a sudo password from the interactive TUI.
751///
752/// **Security:** this event must never carry the password itself — only a
753/// correlation id and UI context (command summary). The secret is delivered
754/// solely through [`crate::runtime::SudoPasswordResolver`].
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
756pub struct SudoPasswordRequest {
757 /// Correlation id for the oneshot resolver.
758 pub id: String,
759 /// Short, non-secret description (e.g. truncated command).
760 pub command_summary: String,
761}
762
763/// Resolution of a sudo password prompt.
764///
765/// Prefer not to serialize responses that still hold a password into logs.
766#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
767#[serde(tag = "kind", rename_all = "snake_case")]
768pub enum SudoPasswordResponse {
769 /// User entered a password. Cleared from memory after bash consumes it.
770 Submitted {
771 id: String,
772 /// Secret — never write to chat history or tool observations.
773 #[serde(skip_serializing)]
774 password: String,
775 },
776 /// User cancelled the modal.
777 Cancelled { id: String },
778}
779
780impl SudoPasswordResponse {
781 pub fn id(&self) -> &str {
782 match self {
783 Self::Submitted { id, .. } | Self::Cancelled { id } => id,
784 }
785 }
786}
787
788/// The security risk category associated with an approval request.
789#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
790pub enum ApprovalRisk {
791 /// Any tool execution in restricted mode.
792 Tool,
793 /// A file write operation.
794 Write,
795 /// A shell command execution.
796 Command,
797 /// A guarded command that requires explicit approval outside YOLO mode.
798 Guarded,
799 /// Loading or executing an external plugin.
800 ExternalPlugin,
801}
802
803/// The outcome of an approval request.
804#[derive(Debug, Clone, Serialize, Deserialize)]
805pub enum ApprovalDecision {
806 /// The user approved the action.
807 Approved {
808 /// Identifier matching the [`ApprovalRequest::id`].
809 id: String,
810 },
811 /// The user denied the action.
812 Denied {
813 /// Identifier matching the [`ApprovalRequest::id`].
814 id: String,
815 },
816}