1use std::collections::HashMap;
7use std::fmt;
8use std::path::Path;
9use std::path::PathBuf;
10use std::str::FromStr;
11use std::time::Duration;
12
13use agcodex_mcp_types::CallToolResult;
14use agcodex_mcp_types::Tool as McpTool;
15use serde::Deserialize;
16use serde::Serialize;
17use serde_bytes::ByteBuf;
18use strum_macros::Display;
19use ts_rs::TS;
20use uuid::Uuid;
21
22use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
23use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
24use crate::message_history::HistoryEntry;
25use crate::parse_command::ParsedCommand;
26use crate::plan_tool::UpdatePlanArgs;
27
28#[derive(Debug, Clone, Deserialize, Serialize)]
30pub struct Submission {
31 pub id: String,
33 pub op: Op,
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
39#[serde(tag = "type", rename_all = "snake_case")]
40#[allow(clippy::large_enum_variant)]
41#[non_exhaustive]
42pub enum Op {
43 Interrupt,
46
47 UserInput {
49 items: Vec<InputItem>,
51 },
52
53 UserTurn {
56 items: Vec<InputItem>,
58
59 cwd: PathBuf,
62
63 approval_policy: AskForApproval,
65
66 sandbox_policy: SandboxPolicy,
68
69 model: String,
72
73 effort: ReasoningEffortConfig,
75
76 summary: ReasoningSummaryConfig,
78 },
79
80 OverrideTurnContext {
86 #[serde(skip_serializing_if = "Option::is_none")]
88 cwd: Option<PathBuf>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 approval_policy: Option<AskForApproval>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 sandbox_policy: Option<SandboxPolicy>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
101 model: Option<String>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 effort: Option<ReasoningEffortConfig>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 summary: Option<ReasoningSummaryConfig>,
110 },
111
112 ExecApproval {
114 id: String,
116 decision: ReviewDecision,
118 },
119
120 PatchApproval {
122 id: String,
124 decision: ReviewDecision,
126 },
127
128 AddToHistory {
133 text: String,
135 },
136
137 GetHistoryEntryRequest { offset: usize, log_id: u64 },
139
140 ListMcpTools,
143
144 Compact,
148 Shutdown,
150}
151
152#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, TS)]
155#[serde(rename_all = "kebab-case")]
156#[strum(serialize_all = "kebab-case")]
157pub enum AskForApproval {
158 #[serde(rename = "untrusted")]
162 #[strum(serialize = "untrusted")]
163 UnlessTrusted,
164
165 OnFailure,
170
171 #[default]
173 OnRequest,
174
175 Never,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, TS)]
182#[strum(serialize_all = "kebab-case")]
183#[serde(tag = "mode", rename_all = "kebab-case")]
184pub enum SandboxPolicy {
185 #[serde(rename = "danger-full-access")]
187 DangerFullAccess,
188
189 #[serde(rename = "read-only")]
191 ReadOnly,
192
193 #[serde(rename = "workspace-write")]
196 WorkspaceWrite {
197 #[serde(default, skip_serializing_if = "Vec::is_empty")]
200 writable_roots: Vec<PathBuf>,
201
202 #[serde(default)]
205 network_access: bool,
206
207 #[serde(default)]
211 exclude_tmpdir_env_var: bool,
212
213 #[serde(default)]
216 exclude_slash_tmp: bool,
217 },
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct WritableRoot {
226 pub root: PathBuf,
228
229 pub read_only_subpaths: Vec<PathBuf>,
231}
232
233impl WritableRoot {
234 pub fn is_path_writable(&self, path: &Path) -> bool {
235 if !path.starts_with(&self.root) {
237 return false;
238 }
239
240 for subpath in &self.read_only_subpaths {
242 if path.starts_with(subpath) {
243 return false;
244 }
245 }
246
247 true
248 }
249}
250
251impl FromStr for SandboxPolicy {
252 type Err = serde_json::Error;
253
254 fn from_str(s: &str) -> Result<Self, Self::Err> {
255 serde_json::from_str(s)
256 }
257}
258
259impl SandboxPolicy {
260 pub const fn new_read_only_policy() -> Self {
262 SandboxPolicy::ReadOnly
263 }
264
265 pub const fn new_workspace_write_policy() -> Self {
269 SandboxPolicy::WorkspaceWrite {
270 writable_roots: vec![],
271 network_access: false,
272 exclude_tmpdir_env_var: false,
273 exclude_slash_tmp: false,
274 }
275 }
276
277 pub const fn has_full_disk_read_access(&self) -> bool {
279 true
280 }
281
282 pub const fn has_full_disk_write_access(&self) -> bool {
283 match self {
284 SandboxPolicy::DangerFullAccess => true,
285 SandboxPolicy::ReadOnly => false,
286 SandboxPolicy::WorkspaceWrite { .. } => false,
287 }
288 }
289
290 pub const fn has_full_network_access(&self) -> bool {
291 match self {
292 SandboxPolicy::DangerFullAccess => true,
293 SandboxPolicy::ReadOnly => false,
294 SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
295 }
296 }
297
298 pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
302 match self {
303 SandboxPolicy::DangerFullAccess => Vec::new(),
304 SandboxPolicy::ReadOnly => Vec::new(),
305 SandboxPolicy::WorkspaceWrite {
306 writable_roots,
307 exclude_tmpdir_env_var,
308 exclude_slash_tmp,
309 network_access: _,
310 } => {
311 let mut roots: Vec<PathBuf> = writable_roots.clone();
313
314 roots.push(cwd.to_path_buf());
317
318 if cfg!(unix) && !exclude_slash_tmp {
320 let slash_tmp = PathBuf::from("/tmp");
321 if slash_tmp.is_dir() {
322 roots.push(slash_tmp);
323 }
324 }
325
326 if !exclude_tmpdir_env_var
335 && let Some(tmpdir) = std::env::var_os("TMPDIR")
336 && !tmpdir.is_empty()
337 {
338 roots.push(PathBuf::from(tmpdir));
339 }
340
341 roots
343 .into_iter()
344 .map(|writable_root| {
345 let mut subpaths = Vec::new();
346 let top_level_git = writable_root.join(".git");
347 if top_level_git.is_dir() {
348 subpaths.push(top_level_git);
349 }
350 WritableRoot {
351 root: writable_root,
352 read_only_subpaths: subpaths,
353 }
354 })
355 .collect()
356 }
357 }
358 }
359}
360
361#[non_exhaustive]
363#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
364#[serde(tag = "type", rename_all = "snake_case")]
365pub enum InputItem {
366 Text {
367 text: String,
368 },
369 Image {
371 image_url: String,
372 },
373
374 LocalImage {
377 path: std::path::PathBuf,
378 },
379}
380
381#[derive(Debug, Clone, Deserialize, Serialize)]
383pub struct Event {
384 pub id: String,
386 pub msg: EventMsg,
388}
389
390#[derive(Debug, Clone, Deserialize, Serialize, Display)]
392#[serde(tag = "type", rename_all = "snake_case")]
393#[strum(serialize_all = "snake_case")]
394pub enum EventMsg {
395 Error(ErrorEvent),
397
398 TaskStarted,
400
401 TaskComplete(TaskCompleteEvent),
403
404 TokenCount(TokenUsage),
407
408 AgentMessage(AgentMessageEvent),
410
411 AgentMessageDelta(AgentMessageDeltaEvent),
413
414 AgentReasoning(AgentReasoningEvent),
416
417 AgentReasoningDelta(AgentReasoningDeltaEvent),
419
420 AgentReasoningRawContent(AgentReasoningRawContentEvent),
422
423 AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
425 AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),
427
428 SessionConfigured(SessionConfiguredEvent),
430
431 McpToolCallBegin(McpToolCallBeginEvent),
432
433 McpToolCallEnd(McpToolCallEndEvent),
434
435 ExecCommandBegin(ExecCommandBeginEvent),
437
438 ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
440
441 ExecCommandEnd(ExecCommandEndEvent),
442
443 ExecApprovalRequest(ExecApprovalRequestEvent),
444
445 ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
446
447 BackgroundEvent(BackgroundEventEvent),
448
449 PatchApplyBegin(PatchApplyBeginEvent),
452
453 PatchApplyEnd(PatchApplyEndEvent),
455
456 TurnDiff(TurnDiffEvent),
457
458 GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
460
461 McpListToolsResponse(McpListToolsResponseEvent),
463
464 PlanUpdate(UpdatePlanArgs),
465
466 TurnAborted(TurnAbortedEvent),
467
468 ShutdownComplete,
470}
471
472#[derive(Debug, Clone, Deserialize, Serialize)]
475pub struct ErrorEvent {
476 pub message: String,
477}
478
479#[derive(Debug, Clone, Deserialize, Serialize)]
480pub struct TaskCompleteEvent {
481 pub last_agent_message: Option<String>,
482}
483
484#[derive(Debug, Clone, Deserialize, Serialize, Default)]
485pub struct TokenUsage {
486 pub input_tokens: u64,
487 pub cached_input_tokens: Option<u64>,
488 pub output_tokens: u64,
489 pub reasoning_output_tokens: Option<u64>,
490 pub total_tokens: u64,
491}
492
493impl TokenUsage {
494 pub const fn is_zero(&self) -> bool {
495 self.total_tokens == 0
496 }
497
498 pub fn cached_input(&self) -> u64 {
499 self.cached_input_tokens.unwrap_or(0)
500 }
501
502 pub fn non_cached_input(&self) -> u64 {
503 self.input_tokens.saturating_sub(self.cached_input())
504 }
505
506 pub fn blended_total(&self) -> u64 {
508 self.non_cached_input() + self.output_tokens
509 }
510
511 pub fn tokens_in_context_window(&self) -> u64 {
516 self.total_tokens
517 .saturating_sub(self.reasoning_output_tokens.unwrap_or(0))
518 }
519
520 pub fn percent_of_context_window_remaining(
531 &self,
532 context_window: u64,
533 baseline_used_tokens: u64,
534 ) -> u8 {
535 if context_window <= baseline_used_tokens {
536 return 0;
537 }
538
539 let effective_window = context_window - baseline_used_tokens;
540 let used = self
541 .tokens_in_context_window()
542 .saturating_sub(baseline_used_tokens);
543 let remaining = effective_window.saturating_sub(used);
544 ((remaining as f32 / effective_window as f32) * 100.0).clamp(0.0, 100.0) as u8
545 }
546}
547
548#[derive(Debug, Clone, Deserialize, Serialize)]
549pub struct FinalOutput {
550 pub token_usage: TokenUsage,
551}
552
553impl From<TokenUsage> for FinalOutput {
554 fn from(token_usage: TokenUsage) -> Self {
555 Self { token_usage }
556 }
557}
558
559impl fmt::Display for FinalOutput {
560 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561 let token_usage = &self.token_usage;
562 write!(
563 f,
564 "Token usage: total={} input={}{} output={}{}",
565 token_usage.blended_total(),
566 token_usage.non_cached_input(),
567 if token_usage.cached_input() > 0 {
568 format!(" (+ {} cached)", token_usage.cached_input())
569 } else {
570 String::new()
571 },
572 token_usage.output_tokens,
573 token_usage
574 .reasoning_output_tokens
575 .map(|r| format!(" (reasoning {r})"))
576 .unwrap_or_default()
577 )
578 }
579}
580
581#[derive(Debug, Clone, Deserialize, Serialize)]
582pub struct AgentMessageEvent {
583 pub message: String,
584}
585
586#[derive(Debug, Clone, Deserialize, Serialize)]
587pub struct AgentMessageDeltaEvent {
588 pub delta: String,
589}
590
591#[derive(Debug, Clone, Deserialize, Serialize)]
592pub struct AgentReasoningEvent {
593 pub text: String,
594}
595
596#[derive(Debug, Clone, Deserialize, Serialize)]
597pub struct AgentReasoningRawContentEvent {
598 pub text: String,
599}
600
601#[derive(Debug, Clone, Deserialize, Serialize)]
602pub struct AgentReasoningRawContentDeltaEvent {
603 pub delta: String,
604}
605
606#[derive(Debug, Clone, Deserialize, Serialize)]
607pub struct AgentReasoningSectionBreakEvent {}
608
609#[derive(Debug, Clone, Deserialize, Serialize)]
610pub struct AgentReasoningDeltaEvent {
611 pub delta: String,
612}
613
614#[derive(Debug, Clone, Deserialize, Serialize)]
615pub struct McpInvocation {
616 pub server: String,
618 pub tool: String,
620 pub arguments: Option<serde_json::Value>,
622}
623
624#[derive(Debug, Clone, Deserialize, Serialize)]
625pub struct McpToolCallBeginEvent {
626 pub call_id: String,
628 pub invocation: McpInvocation,
629}
630
631#[derive(Debug, Clone, Deserialize, Serialize)]
632pub struct McpToolCallEndEvent {
633 pub call_id: String,
635 pub invocation: McpInvocation,
636 pub duration: Duration,
637 pub result: Result<CallToolResult, String>,
639}
640
641impl McpToolCallEndEvent {
642 pub fn is_success(&self) -> bool {
643 match &self.result {
644 Ok(result) => !result.is_error.unwrap_or(false),
645 Err(_) => false,
646 }
647 }
648}
649
650#[derive(Debug, Clone, Deserialize, Serialize)]
651pub struct ExecCommandBeginEvent {
652 pub call_id: String,
654 pub command: Vec<String>,
656 pub cwd: PathBuf,
658 pub parsed_cmd: Vec<ParsedCommand>,
659}
660
661#[derive(Debug, Clone, Deserialize, Serialize)]
662pub struct ExecCommandEndEvent {
663 pub call_id: String,
665 pub stdout: String,
667 pub stderr: String,
669 pub exit_code: i32,
671 pub duration: Duration,
673}
674
675#[derive(Debug, Clone, Deserialize, Serialize)]
676#[serde(rename_all = "snake_case")]
677pub enum ExecOutputStream {
678 Stdout,
679 Stderr,
680}
681
682#[derive(Debug, Clone, Deserialize, Serialize)]
683pub struct ExecCommandOutputDeltaEvent {
684 pub call_id: String,
686 pub stream: ExecOutputStream,
688 #[serde(with = "serde_bytes")]
690 pub chunk: ByteBuf,
691}
692
693#[derive(Debug, Clone, Deserialize, Serialize)]
694pub struct ExecApprovalRequestEvent {
695 pub call_id: String,
697 pub command: Vec<String>,
699 pub cwd: PathBuf,
701 #[serde(skip_serializing_if = "Option::is_none")]
703 pub reason: Option<String>,
704}
705
706#[derive(Debug, Clone, Deserialize, Serialize)]
707pub struct ApplyPatchApprovalRequestEvent {
708 pub call_id: String,
710 pub changes: HashMap<PathBuf, FileChange>,
711 #[serde(skip_serializing_if = "Option::is_none")]
713 pub reason: Option<String>,
714 #[serde(skip_serializing_if = "Option::is_none")]
716 pub grant_root: Option<PathBuf>,
717}
718
719#[derive(Debug, Clone, Deserialize, Serialize)]
720pub struct BackgroundEventEvent {
721 pub message: String,
722}
723
724#[derive(Debug, Clone, Deserialize, Serialize)]
725pub struct PatchApplyBeginEvent {
726 pub call_id: String,
728 pub auto_approved: bool,
730 pub changes: HashMap<PathBuf, FileChange>,
732}
733
734#[derive(Debug, Clone, Deserialize, Serialize)]
735pub struct PatchApplyEndEvent {
736 pub call_id: String,
738 pub stdout: String,
740 pub stderr: String,
742 pub success: bool,
744}
745
746#[derive(Debug, Clone, Deserialize, Serialize)]
747pub struct TurnDiffEvent {
748 pub unified_diff: String,
749}
750
751#[derive(Debug, Clone, Deserialize, Serialize)]
752pub struct GetHistoryEntryResponseEvent {
753 pub offset: usize,
754 pub log_id: u64,
755 #[serde(skip_serializing_if = "Option::is_none")]
757 pub entry: Option<HistoryEntry>,
758}
759
760#[derive(Debug, Clone, Deserialize, Serialize)]
762pub struct McpListToolsResponseEvent {
763 pub tools: std::collections::HashMap<String, McpTool>,
765}
766
767#[derive(Debug, Default, Clone, Deserialize, Serialize)]
768pub struct SessionConfiguredEvent {
769 pub session_id: Uuid,
771
772 pub model: String,
774
775 pub history_log_id: u64,
777
778 pub history_entry_count: usize,
780}
781
782#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS)]
784#[serde(rename_all = "snake_case")]
785pub enum ReviewDecision {
786 Approved,
788
789 ApprovedForSession,
793
794 #[default]
797 Denied,
798
799 Abort,
802}
803
804#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
805#[serde(rename_all = "snake_case")]
806pub enum FileChange {
807 Add {
808 content: String,
809 },
810 Delete,
811 Update {
812 unified_diff: String,
813 move_path: Option<PathBuf>,
814 },
815}
816
817#[derive(Debug, Clone, Deserialize, Serialize)]
818pub struct Chunk {
819 pub orig_index: u32,
821 pub deleted_lines: Vec<String>,
822 pub inserted_lines: Vec<String>,
823}
824
825#[derive(Debug, Clone, Deserialize, Serialize)]
826pub struct TurnAbortedEvent {
827 pub reason: TurnAbortReason,
828}
829
830#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
831#[serde(rename_all = "snake_case")]
832pub enum TurnAbortReason {
833 Interrupted,
834 Replaced,
835}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840
841 #[test]
844 fn serialize_event() {
845 let session_id: Uuid = uuid::uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8");
846 let event = Event {
847 id: "1234".to_string(),
848 msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
849 session_id,
850 model: "agcodex-mini-latest".to_string(),
851 history_log_id: 0,
852 history_entry_count: 0,
853 }),
854 };
855 let serialized = serde_json::to_string(&event).unwrap();
856 assert_eq!(
857 serialized,
858 r#"{"id":"1234","msg":{"type":"session_configured","session_id":"67e55044-10b1-426f-9247-bb680e5fe0c8","model":"agcodex-mini-latest","history_log_id":0,"history_entry_count":0}}"#
859 );
860 }
861}