1use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16pub mod method {
18 pub const LAUNCH: &str = "engine/launch";
20 pub const ONE_SHOT: &str = "engine/oneShot";
22 pub const ONE_SHOT_READ: &str = "engine/oneShotRead";
24 pub const ATTACH: &str = "agent/attach";
26 pub const DETACH: &str = "agent/detach";
28 pub const FRAMES: &str = "agent/frames";
30 pub const SEND_MESSAGE: &str = "agent/sendMessage";
32 pub const INTERRUPT: &str = "agent/interrupt";
34 pub const SET_MODEL: &str = "agent/setModel";
36 pub const SET_EFFORT: &str = "agent/setEffort";
37 pub const SET_MODE: &str = "agent/setMode";
38 pub const ANSWER_PERMISSION: &str = "agent/answerPermission";
40 pub const ANSWER_QUESTION: &str = "agent/answerQuestion";
41 pub const READ_USAGE: &str = "agent/readUsage";
43 pub const TERMINAL_LAUNCH: &str = "terminal/launch";
45 pub const TERMINAL_HOOK: &str = "terminal/hook";
47 pub const OPEN: &str = "agent/open";
49 pub const CLOSE: &str = "agent/close";
52 pub const LET_GO: &str = "agent/letGo";
54 pub const COMPACT: &str = "agent/compact";
56 pub const RENAME: &str = "agent/rename";
58 pub const CONTROL: &str = "agent/control";
60 pub const ITEMS: &str = "agent/items";
63 pub const PROGRAM_GONE: &str = "agent/programGone";
66 pub const STOP_TASK: &str = "agent/stopTask";
68 pub const TASK_OUTPUT: &str = "agent/taskOutput";
70 pub const REMOTE: &str = "agent/remote";
72}
73
74pub const BATCH_BYTES: usize = 64 * 1024;
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
80#[serde(tag = "kind", rename_all = "camelCase")]
81pub enum StartAt {
82 #[default]
83 New,
84 Resume {
85 session: String,
86 },
87 Fork {
88 of: String,
89 #[serde(default)]
90 at: Option<String>,
91 },
92}
93
94#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct Grant {
98 pub url: String,
99 pub token: String,
100}
101
102impl std::fmt::Debug for Grant {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("Grant")
105 .field("url", &self.url)
106 .field("token", &"…")
107 .finish()
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
113#[serde(rename_all = "camelCase")]
114pub struct LaunchParams {
115 pub agent: String,
117 pub cwd: String,
118 pub model: String,
119 #[serde(default)]
120 pub effort: String,
121 pub mode: String,
122 #[serde(default)]
123 pub append_prompt: Option<String>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub relay: Option<String>,
128 #[serde(default)]
130 pub by_key: bool,
131 #[serde(default)]
132 pub start: StartAt,
133 #[serde(default)]
134 pub read_dirs: Vec<String>,
135 #[serde(default)]
137 pub escouade: Option<Grant>,
138 #[serde(default)]
141 pub fence: Vec<String>,
142 #[serde(default)]
144 pub read_only: bool,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub escouade_file: Option<String>,
148 #[serde(default)]
150 pub program: String,
151 #[serde(default)]
153 pub home: Option<String>,
154 #[serde(default)]
157 pub described: Option<HarnessDecl>,
158 #[serde(default)]
160 pub lang: String,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub takes_effort: Option<bool>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub settings: Option<String>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub refusal_file: Option<String>,
171}
172
173#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct EnvVar {
176 pub name: String,
177 pub value: String,
178}
179
180impl std::fmt::Debug for EnvVar {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 write!(f, "{}=…", self.name)
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct FileToWrite {
189 pub path: String,
190 pub text: String,
191}
192
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
196#[serde(rename_all = "camelCase")]
197pub struct Launch {
198 #[serde(default)]
199 pub args: Vec<String>,
200 #[serde(default)]
201 pub env: Vec<EnvVar>,
202 #[serde(default)]
204 pub remove_env: Vec<String>,
205 #[serde(default)]
206 pub files: Vec<FileToWrite>,
207 #[serde(default)]
209 pub fence_applied: bool,
210 #[serde(default)]
212 pub read_only: bool,
213 #[serde(default)]
217 pub fence_by_client: bool,
218 #[serde(default)]
220 pub writes: Vec<String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub shared: Option<SharedLaunch>,
225 #[serde(default)]
227 pub trust: Vec<String>,
228}
229
230#[derive(Clone, PartialEq, Serialize, Deserialize, Default)]
233#[serde(rename_all = "camelCase")]
234pub struct SharedLaunch {
235 pub ready: String,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub listens: Option<String>,
241 #[serde(default)]
243 pub one_agent_a_folder: bool,
244 #[serde(default)]
246 pub keep: Value,
247}
248
249impl std::fmt::Debug for SharedLaunch {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 f.debug_struct("SharedLaunch")
252 .field("ready", &self.ready)
253 .field("listens", &self.listens)
254 .field("one_agent_a_folder", &self.one_agent_a_folder)
255 .field("keep", &"…")
256 .finish()
257 }
258}
259
260#[derive(Clone, PartialEq, Serialize, Deserialize, Default)]
262pub struct SharedRef {
263 pub key: String,
265 pub line: String,
267 #[serde(default)]
269 pub keep: Value,
270}
271
272impl std::fmt::Debug for SharedRef {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 f.debug_struct("SharedRef")
275 .field("key", &self.key)
276 .field("line", &self.line)
277 .field("keep", &"…")
278 .finish()
279 }
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
284#[serde(rename_all = "camelCase")]
285pub struct Attach {
286 pub agent: String,
287 pub cwd: String,
288 #[serde(default)]
289 pub session: Option<String>,
290 #[serde(default)]
291 pub model: String,
292 #[serde(default)]
293 pub effort: String,
294 #[serde(default)]
295 pub mode: String,
296 #[serde(default)]
298 pub pending: Vec<String>,
299 #[serde(default)]
301 pub described: Option<HarnessDecl>,
302 #[serde(default)]
304 pub lang: String,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub shared: Option<SharedRef>,
308 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
311 pub roots: std::collections::BTreeMap<String, String>,
312 #[serde(default, skip_serializing_if = "Vec::is_empty")]
315 pub fence: Vec<String>,
316 #[serde(default)]
317 pub read_only: bool,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub append_prompt: Option<String>,
320 #[serde(default)]
322 pub running: bool,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub home: Option<String>,
327 #[serde(default, skip_serializing_if = "Vec::is_empty")]
328 pub read_dirs: Vec<String>,
329 #[serde(default)]
330 pub by_key: bool,
331}
332
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
335pub struct Open {
336 pub agent: String,
337 #[serde(default)]
338 pub start: StartAt,
339 #[serde(default)]
341 pub escouade: Option<Grant>,
342}
343
344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
346pub struct Opened {
347 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub attach: Option<Launch>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub catalog: Option<Value>,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356pub struct Close {
357 pub agent: String,
358 #[serde(default)]
360 pub now: bool,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
364pub struct Compact {
365 pub agent: String,
366 #[serde(default)]
367 pub instructions: String,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
372pub struct Compacted {
373 #[serde(default)]
374 pub over: bool,
375 #[serde(default, skip_serializing_if = "Vec::is_empty")]
377 pub writes: Vec<String>,
378 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
380 pub turn: bool,
381}
382
383#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct Control {
386 pub agent: String,
387 pub request: Value,
388 #[serde(default)]
390 pub within_ms: u64,
391}
392
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
395pub struct Pushed {
396 pub agent: String,
397 pub items: Vec<Item>,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct AgentRef {
402 pub agent: String,
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
407#[serde(rename_all = "camelCase")]
408pub struct Interrupt {
409 pub agent: String,
410 #[serde(default)]
412 pub cancel_queued: bool,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417pub struct Gone {
418 pub agent: String,
419 #[serde(default)]
420 pub code: Option<i32>,
421 #[serde(default)]
422 pub said: String,
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427pub struct Frames {
428 pub agent: String,
429 pub frames: Vec<String>,
430}
431
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
435pub struct Items {
436 #[serde(default)]
437 pub items: Vec<Item>,
438}
439
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
442pub struct Writes {
443 #[serde(default)]
444 pub writes: Vec<String>,
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
448pub struct SendMessage {
449 pub agent: String,
450 pub uuid: String,
451 pub text: String,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub content: Option<serde_json::Value>,
456}
457
458#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
459pub struct SetValue {
460 pub agent: String,
461 pub value: String,
462}
463
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct AnswerPermission {
467 pub agent: String,
468 pub request: String,
469 pub allow: bool,
470 #[serde(default)]
472 pub always: bool,
473 #[serde(default)]
475 pub input: Value,
476 #[serde(default)]
478 pub message: Option<String>,
479 #[serde(default)]
481 pub resume: Option<String>,
482 #[serde(default, skip_serializing_if = "String::is_empty")]
484 pub call: String,
485 #[serde(default, skip_serializing_if = "Value::is_null")]
487 pub rules: Value,
488}
489
490#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
491pub struct AnswerQuestion {
492 pub agent: String,
493 pub request: String,
494 pub call: String,
495 pub answers: Value,
497 #[serde(default)]
499 pub questions: Value,
500 #[serde(default, skip_serializing_if = "Value::is_null")]
502 pub input: Value,
503}
504
505#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
507pub struct Usage {
508 #[serde(default)]
509 pub usage: Value,
510}
511
512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
515pub struct UsageAsked {
516 #[serde(default)]
517 pub writes: Vec<String>,
518}
519
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
523pub struct Asked {
524 #[serde(default)]
525 pub writes: Vec<String>,
526 #[serde(default)]
527 pub ask: String,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532pub struct Task {
533 pub agent: String,
534 pub task: String,
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct RemoteLink {
542 pub agent: String,
543 pub enabled: bool,
544 #[serde(default)]
545 pub name: String,
546 #[serde(default)]
547 pub keep: bool,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub reattach: Option<String>,
550}
551
552#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
554pub struct ModelTurn {
555 pub model: String,
556 #[serde(default)]
557 pub input: u64,
558 #[serde(default)]
559 pub output: u64,
560 #[serde(default)]
561 pub cache: u64,
562 #[serde(default)]
563 pub cost: f64,
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
568pub struct TurnUsage {
569 #[serde(default)]
570 pub input: u64,
571 #[serde(default)]
572 pub output: u64,
573 #[serde(default)]
574 pub cache: u64,
575 #[serde(default)]
576 pub cost: f64,
577}
578
579#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
583#[serde(tag = "op", rename_all = "camelCase")]
584pub enum Item {
585 Start {
587 #[serde(default)]
588 session: Option<String>,
589 },
590 Session { session: String },
593 #[serde(rename_all = "camelCase")]
595 Text {
596 text: String,
597 #[serde(default)]
598 parent: Option<String>,
599 #[serde(default)]
600 id: Option<String>,
601 },
602 Thinking {
603 text: String,
604 #[serde(default)]
605 parent: Option<String>,
606 #[serde(default)]
607 id: Option<String>,
608 },
609 Delta {
612 id: String,
613 text: String,
614 #[serde(default)]
615 thinking: bool,
616 #[serde(default)]
617 parent: Option<String>,
618 },
619 Subagent { call: String, model: String },
621 Tool {
623 id: String,
624 name: String,
625 #[serde(default)]
626 input: Value,
627 #[serde(default)]
628 parent: Option<String>,
629 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
631 unplanned: bool,
632 },
633 ToolInput {
636 id: String,
637 #[serde(default)]
638 name: Option<String>,
639 #[serde(default)]
640 input: Value,
641 },
642 ToolDone {
645 id: String,
646 #[serde(default)]
647 output: String,
648 #[serde(default)]
649 error: bool,
650 #[serde(default)]
651 result: Value,
652 },
653 Touched { path: String },
655 #[serde(rename_all = "camelCase")]
659 Permission {
660 request: String,
661 call: String,
662 #[serde(default)]
663 tool: Option<String>,
664 #[serde(default)]
665 input: Option<Value>,
666 #[serde(default)]
667 description: Option<String>,
668 #[serde(default)]
669 reason: Option<String>,
670 #[serde(default)]
671 diff: Option<Diff>,
672 #[serde(default)]
673 can_always: bool,
674 #[serde(default)]
676 parent: Option<String>,
677 #[serde(default, skip_serializing_if = "Option::is_none")]
679 hunks: Option<Value>,
680 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
682 always_by_harness: bool,
683 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
685 ends_stream: bool,
686 },
687 Question {
689 request: String,
690 call: String,
691 questions: Value,
692 #[serde(default, skip_serializing_if = "Option::is_none")]
694 parent: Option<String>,
695 },
696 Answered {
699 request: String,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
701 decision: Option<String>,
702 #[serde(default, skip_serializing_if = "Option::is_none")]
703 answers: Option<Value>,
704 },
705 User { id: String, text: String },
707 Model { model: String },
709 Context {
711 #[serde(default, skip_serializing_if = "Option::is_none")]
712 tokens: Option<u64>,
713 #[serde(default, skip_serializing_if = "Option::is_none")]
714 window: Option<u64>,
715 },
716 Todos { todos: Value },
719 Mode { mode: String },
721 Turn {
723 #[serde(default)]
724 interrupted: bool,
725 #[serde(
728 default,
729 deserialize_with = "present",
730 skip_serializing_if = "Option::is_none"
731 )]
732 limited: Option<Option<i64>>,
733 #[serde(default)]
734 usage: TurnUsage,
735 #[serde(default)]
737 error: Option<String>,
738 #[serde(default, skip_serializing_if = "Option::is_none")]
740 entry: Option<String>,
741 },
742 Notice {
744 text: String,
745 #[serde(default = "info")]
746 level: String,
747 },
748 Write { line: String },
750 #[serde(rename_all = "camelCase")]
753 PermissionCard {
754 request: String,
755 call: String,
756 tool: String,
757 #[serde(default)]
758 input: Value,
759 #[serde(default)]
760 description: Option<String>,
761 #[serde(default)]
762 reason: Option<String>,
763 #[serde(default)]
764 hunks: Value,
765 #[serde(default)]
766 can_always: bool,
767 #[serde(default)]
768 always_scope: Option<String>,
769 #[serde(default)]
770 parent: Option<String>,
771 },
772 QuestionCard {
774 request: String,
775 call: String,
776 questions: Value,
777 #[serde(default)]
778 form: Value,
779 #[serde(default)]
780 parent: Option<String>,
781 },
782 Quota {
784 #[serde(default)]
785 meters: Value,
786 },
787 Usage {
789 #[serde(default)]
790 usage: Value,
791 },
792 LimitRest,
794 Ready {
797 #[serde(default)]
798 models: Value,
799 },
800 Compacted {
802 #[serde(default)]
803 error: Option<String>,
804 },
805 Stop { reason: String },
807 Entry {
809 #[serde(default)]
810 entry: Option<String>,
811 },
812 Activity {
815 #[serde(default)]
816 tool: Option<String>,
817 #[serde(default)]
818 input: Value,
819 #[serde(default)]
820 block: Option<String>,
821 },
822 Took { ms: i64 },
825 Relaunch { launch: Box<Launch> },
828 Pause,
830 File { path: String, text: String },
832 Closed,
834 Conversation {
837 session: String,
838 #[serde(default)]
839 notice: String,
840 },
841 Message {
844 id: String,
845 #[serde(default)]
846 model: String,
847 #[serde(default)]
848 usage: Value,
849 #[serde(default)]
850 main: bool,
851 },
852 MessageOutput { id: String, output: u64 },
854 Tasks { frame: Value },
856 Event {
859 #[serde(default)]
860 id: Option<String>,
861 source: String,
862 text: String,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
864 from: Option<String>,
865 },
866 Output {
868 #[serde(default)]
869 id: Option<String>,
870 level: String,
871 text: String,
872 },
873 Call {
876 id: String,
877 name: String,
878 #[serde(default)]
879 input: Value,
880 #[serde(default)]
881 parent: Option<String>,
882 },
883 ToolResult {
885 id: String,
886 #[serde(default)]
887 output: String,
888 #[serde(default)]
889 error: bool,
890 #[serde(default)]
891 result: Value,
892 },
893 Replayed {
896 #[serde(default)]
897 id: Option<String>,
898 #[serde(default)]
899 text: String,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
901 origin: Option<String>,
902 #[serde(default, skip_serializing_if = "Option::is_none")]
903 from: Option<String>,
904 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
905 synthetic: bool,
906 #[serde(default)]
907 images: u32,
908 #[serde(default, skip_serializing_if = "Vec::is_empty")]
909 files: Vec<String>,
910 },
911 #[serde(rename_all = "camelCase")]
915 Ask {
916 request: String,
917 call: String,
918 tool: String,
919 #[serde(default)]
920 input: Value,
921 #[serde(default)]
922 title: Option<String>,
923 #[serde(default)]
924 description: Option<String>,
925 #[serde(default)]
926 reason: Option<String>,
927 #[serde(default)]
928 suggestions: Value,
929 #[serde(default)]
930 no_always: bool,
931 #[serde(default)]
932 default_no: bool,
933 #[serde(default)]
934 task: Option<String>,
935 },
936 #[serde(rename_all = "camelCase")]
941 Result {
942 #[serde(default)]
943 id: Option<String>,
944 #[serde(default)]
945 failed: bool,
946 #[serde(default)]
947 error: Option<String>,
948 #[serde(default)]
949 failure: String,
950 #[serde(default)]
951 reply: String,
952 #[serde(default)]
953 limited: bool,
954 #[serde(default)]
955 resets: Option<i64>,
956 #[serde(default)]
957 sign_in: bool,
958 #[serde(default)]
959 duration: Value,
960 #[serde(default)]
961 window: Option<u64>,
962 #[serde(default)]
963 cost: f64,
964 #[serde(default)]
965 tokens: u64,
966 #[serde(default)]
967 models: Vec<ModelTurn>,
968 },
969 Limit {
971 #[serde(default)]
972 scoped: bool,
973 },
974 Overage { on: bool },
976 Remote {
978 #[serde(default)]
979 state: Option<String>,
980 },
981 Told {
983 #[serde(default)]
984 commands: Value,
985 #[serde(default)]
986 models: Value,
987 },
988 Answer {
990 ask: String,
991 #[serde(default)]
992 answer: Value,
993 #[serde(default, skip_serializing_if = "Option::is_none")]
994 error: Option<String>,
995 },
996 Said { line: String },
998 #[serde(other)]
1000 Unknown,
1001}
1002
1003fn info() -> String {
1004 "info".into()
1005}
1006
1007fn present<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<Option<i64>>, D::Error> {
1009 Ok(Some(Option::<i64>::deserialize(d)?))
1010}
1011
1012pub fn hunks_of(diff: &str) -> Value {
1014 let mut hunks: Vec<Value> = Vec::new();
1015 for line in diff.lines() {
1016 if let Some(head) = line.strip_prefix("@@ ") {
1017 let num = |sign: char| {
1018 head.split_whitespace()
1019 .find_map(|w| w.strip_prefix(sign))
1020 .and_then(|w| w.split(',').next())
1021 .and_then(|n| n.parse::<u64>().ok())
1022 .unwrap_or(0)
1023 };
1024 hunks.push(
1025 serde_json::json!({ "oldStart": num('-'), "newStart": num('+'), "lines": [] }),
1026 );
1027 continue;
1028 }
1029 if line.starts_with("+++") || line.starts_with("---") {
1030 continue;
1031 }
1032 if hunks.is_empty() {
1033 if !(line.starts_with('+') || line.starts_with('-') || line.starts_with(' ')) {
1034 continue;
1035 }
1036 hunks.push(serde_json::json!({ "oldStart": 1, "newStart": 1, "lines": [] }));
1037 }
1038 if let Some(lines) = hunks.last_mut().and_then(|h| h["lines"].as_array_mut()) {
1039 lines.push(serde_json::json!(line));
1040 }
1041 }
1042 Value::Array(hunks)
1043}
1044
1045#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1047#[serde(rename_all = "camelCase")]
1048pub struct Diff {
1049 pub path: String,
1050 #[serde(default)]
1051 pub old_text: Option<String>,
1052 #[serde(default)]
1053 pub new_text: String,
1054}
1055
1056#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1058#[serde(rename_all = "camelCase")]
1059pub struct OneShotParams {
1060 pub weight: String,
1062 pub cwd: String,
1063 pub system: String,
1064 pub prompt: String,
1065 #[serde(default)]
1066 pub model: String,
1067}
1068
1069#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1071#[serde(rename_all = "camelCase")]
1072pub struct OneShotLaunch {
1073 #[serde(default)]
1074 pub args: Vec<String>,
1075 #[serde(default)]
1076 pub env: Vec<EnvVar>,
1077 #[serde(default)]
1078 pub remove_env: Vec<String>,
1079 #[serde(default)]
1080 pub input: Option<String>,
1081}
1082
1083#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1084pub struct OneShotOutput {
1085 pub stdout: String,
1086 pub stderr: String,
1087 #[serde(default)]
1088 pub code: Option<i32>,
1089}
1090
1091#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1092pub struct OneShotText {
1093 pub text: String,
1094}
1095
1096#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1098#[serde(rename_all = "camelCase")]
1099pub struct TerminalParams {
1100 pub agent: String,
1101 pub cwd: String,
1102 #[serde(default)]
1103 pub session: Option<String>,
1104 #[serde(default)]
1105 pub model: String,
1106 #[serde(default)]
1107 pub mode: String,
1108 #[serde(default)]
1109 pub home: Option<String>,
1110 pub hook_url: String,
1113 pub hook_token_var: String,
1114 #[serde(default)]
1115 pub hook_command: Option<String>,
1116 #[serde(default)]
1117 pub escouade: Option<Grant>,
1118 #[serde(default)]
1119 pub fence: Vec<String>,
1120 #[serde(default)]
1121 pub read_only: bool,
1122}
1123
1124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1126pub struct Hook {
1127 pub agent: String,
1128 pub hook: Value,
1129 #[serde(default)]
1130 pub reply: Option<HookReply>,
1131}
1132
1133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1135pub struct HookReply {
1136 #[serde(default)]
1138 pub context: Option<String>,
1139 #[serde(default)]
1141 pub deny: Option<String>,
1142}
1143
1144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1146pub struct HookAnswer {
1147 #[serde(default)]
1148 pub items: Vec<Item>,
1149 #[serde(default)]
1150 pub answer: Option<Value>,
1151}
1152
1153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1156#[serde(rename_all = "camelCase")]
1157pub struct HarnessDecl {
1158 pub id: String,
1159 pub name: String,
1160 #[serde(default)]
1161 pub speaker: Option<String>,
1162 #[serde(default)]
1164 pub locate: Locate,
1165 #[serde(default = "version_args")]
1166 pub version_args: Vec<String>,
1167 #[serde(default)]
1168 pub tested: TestedDecl,
1169 #[serde(default)]
1170 pub caps: CapsDecl,
1171 #[serde(default)]
1172 pub models: Vec<ModelDecl>,
1173 #[serde(default)]
1174 pub efforts: Vec<String>,
1175 #[serde(default)]
1177 pub account_var: Option<String>,
1178 #[serde(default)]
1181 pub key_var: Option<String>,
1182 #[serde(default)]
1184 pub close_grace_ms: Option<u64>,
1185 #[serde(default)]
1188 pub framing: Option<String>,
1189 #[serde(default)]
1191 pub close_line: Option<String>,
1192 #[serde(default)]
1195 pub opens: bool,
1196 #[serde(default)]
1199 pub relaunches: bool,
1200 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1202 pub stderr_leaves_out: Vec<String>,
1203 #[serde(default)]
1206 pub memory: Option<MemoryDecl>,
1207 #[serde(default)]
1210 pub acp: Option<AcpDecl>,
1211 #[serde(default)]
1213 pub install: Option<InstallDecl>,
1214 #[serde(default)]
1216 pub update_command: Option<String>,
1217 #[serde(default)]
1219 pub writer: Option<String>,
1220}
1221
1222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1225#[serde(untagged)]
1226pub enum Text {
1227 One(String),
1228 ByLang(std::collections::BTreeMap<String, String>),
1229}
1230
1231impl Text {
1232 pub fn get(&self, lang: &str) -> String {
1234 match self {
1235 Text::One(t) => t.clone(),
1236 Text::ByLang(m) => m
1237 .get(lang)
1238 .or_else(|| m.get("en"))
1239 .or_else(|| m.values().next())
1240 .cloned()
1241 .unwrap_or_default(),
1242 }
1243 }
1244}
1245
1246#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1249pub struct ModeDecl {
1250 pub id: String,
1251 #[serde(default, skip_serializing_if = "Option::is_none")]
1252 pub meaning: Option<Text>,
1253}
1254
1255impl<'de> Deserialize<'de> for ModeDecl {
1256 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1257 #[derive(Deserialize)]
1258 #[serde(untagged)]
1259 enum Form {
1260 Id(String),
1261 Full {
1262 id: String,
1263 #[serde(default)]
1264 meaning: Option<Text>,
1265 },
1266 }
1267 Ok(match Form::deserialize(d)? {
1268 Form::Id(id) => ModeDecl { id, meaning: None },
1269 Form::Full { id, meaning } => ModeDecl { id, meaning },
1270 })
1271 }
1272}
1273
1274impl From<&str> for ModeDecl {
1275 fn from(id: &str) -> Self {
1276 ModeDecl {
1277 id: id.into(),
1278 meaning: None,
1279 }
1280 }
1281}
1282
1283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1285pub struct InstallDecl {
1286 #[serde(default)]
1287 pub windows: String,
1288 #[serde(default)]
1289 pub unix: String,
1290}
1291
1292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1294#[serde(rename_all = "camelCase")]
1295pub struct AcpDecl {
1296 #[serde(default)]
1298 pub args: Vec<String>,
1299 #[serde(default)]
1301 pub env: std::collections::BTreeMap<String, String>,
1302 #[serde(default)]
1304 pub remove_env: Vec<String>,
1305 #[serde(default = "yes")]
1308 pub fs: bool,
1309 #[serde(default)]
1313 pub fence_on_permissions: bool,
1314 #[serde(default)]
1316 pub modes: std::collections::BTreeMap<String, String>,
1317 #[serde(default)]
1319 pub read_only_mode: Option<String>,
1320 #[serde(default)]
1323 pub model_option: Option<String>,
1324 #[serde(default)]
1327 pub extensions: Vec<String>,
1328 #[serde(default)]
1331 pub questions_in_text: bool,
1332 #[serde(default)]
1335 pub subagent_tools: Vec<String>,
1336 #[serde(default)]
1339 pub tools: std::collections::BTreeMap<String, String>,
1340}
1341
1342fn yes() -> bool {
1343 true
1344}
1345
1346fn version_args() -> Vec<String> {
1347 vec!["--version".into()]
1348}
1349
1350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1351pub struct Locate {
1352 #[serde(default)]
1353 pub program: Option<String>,
1354 #[serde(default)]
1355 pub names: Vec<String>,
1356}
1357
1358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1359pub struct TestedDecl {
1360 pub oldest: String,
1361 pub newest: String,
1362}
1363
1364impl Default for TestedDecl {
1365 fn default() -> Self {
1366 Self {
1367 oldest: "0".into(),
1368 newest: "0".into(),
1369 }
1370 }
1371}
1372
1373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1375#[serde(rename_all = "camelCase")]
1376pub struct CapsDecl {
1377 #[serde(default)]
1378 pub modes: Vec<ModeDecl>,
1379 #[serde(default)]
1380 pub effort: bool,
1381 #[serde(default)]
1382 pub images: bool,
1383 #[serde(default)]
1384 pub pdf: bool,
1385 #[serde(default)]
1386 pub slash_commands: bool,
1387 #[serde(default)]
1388 pub terminal_view: bool,
1389 #[serde(default)]
1392 pub read_only: bool,
1393 #[serde(default)]
1395 pub accounts_by_key: bool,
1396}
1397
1398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1399pub struct ModelDecl {
1400 pub id: String,
1401 #[serde(default)]
1402 pub label: Option<String>,
1403 #[serde(default)]
1404 pub effort: bool,
1405}
1406
1407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1408pub struct MemoryDecl {
1409 pub dir: String,
1410}
1411
1412#[cfg(test)]
1413mod tests {
1414 use super::*;
1415 use serde_json::json;
1416
1417 #[test]
1418 fn an_item_of_an_unknown_op_is_left_aside_and_the_known_ones_read() {
1419 let items: Items = serde_json::from_value(json!({ "items": [
1420 { "op": "text", "text": "Bonjour" },
1421 { "op": "later", "what": 1 },
1422 { "op": "turn", "usage": { "input": 3 } },
1423 ] }))
1424 .unwrap();
1425 assert_eq!(
1426 items.items[0],
1427 Item::Text {
1428 text: "Bonjour".into(),
1429 parent: None,
1430 id: None,
1431 }
1432 );
1433 assert_eq!(items.items[1], Item::Unknown);
1434 assert!(matches!(items.items[2], Item::Turn { usage, .. } if usage.input == 3));
1435 }
1436
1437 #[test]
1438 fn a_grant_and_a_variable_never_print_their_secret() {
1439 let g = Grant {
1440 url: "http://127.0.0.1:1/mcp".into(),
1441 token: "secret".into(),
1442 };
1443 let v = EnvVar {
1444 name: "TOKEN".into(),
1445 value: "secret".into(),
1446 };
1447 assert!(!format!("{g:?}{v:?}").contains("secret"));
1448 }
1449
1450 #[test]
1451 fn a_declaration_takes_its_defaults() {
1452 let d: HarnessDecl = serde_json::from_value(json!({ "id": "x", "name": "X" })).unwrap();
1453 assert_eq!(d.version_args, ["--version"]);
1454 assert_eq!(d.tested.oldest, "0");
1455 assert!(!d.caps.read_only && d.models.is_empty());
1456 assert!(d.acp.is_none() && d.install.is_none());
1457 }
1458
1459 #[test]
1460 fn an_acp_harness_reads_with_its_modes_by_id_or_with_their_meaning() {
1461 let d: HarnessDecl = serde_json::from_value(json!({
1462 "id": "goose", "name": "Goose",
1463 "caps": { "modes": ["default", { "id": "bypassPermissions", "meaning": { "fr": "Tout", "en": "All" } }] },
1464 "acp": { "args": ["acp"], "modes": { "bypassPermissions": "auto" } },
1465 "install": { "unix": "curl …" },
1466 }))
1467 .unwrap();
1468 assert_eq!(d.caps.modes[0], ModeDecl::from("default"));
1469 assert_eq!(d.caps.modes[1].meaning.as_ref().unwrap().get("fr"), "Tout");
1470 assert_eq!(d.caps.modes[1].meaning.as_ref().unwrap().get("de"), "All");
1471 let acp = d.acp.unwrap();
1472 assert!(acp.fs, "the files go through the client unless it says not");
1473 assert_eq!(acp.args, ["acp"]);
1474 assert_eq!(acp.modes["bypassPermissions"], "auto");
1475 let items: Items = serde_json::from_value(json!({ "items": [
1477 { "op": "toolInput", "id": "c", "input": { "file_path": "a" } },
1478 { "op": "permission", "request": "r", "call": "c", "canAlways": true, "diff": { "path": "a", "newText": "x" } },
1479 { "op": "permission", "request": "r2", "call": "c2" },
1480 { "op": "todos", "todos": [] },
1481 { "op": "turn", "error": "refusé" },
1482 ] }))
1483 .unwrap();
1484 assert!(
1485 matches!(&items.items[1], Item::Permission { can_always: true, diff: Some(d), .. } if d.old_text.is_none())
1486 );
1487 assert!(matches!(&items.items[4], Item::Turn { error: Some(e), .. } if e == "refusé"));
1488 for limited in [None, Some(None), Some(Some(5))] {
1490 let turn = Item::Turn {
1491 interrupted: false,
1492 limited,
1493 usage: TurnUsage::default(),
1494 error: None,
1495 entry: None,
1496 };
1497 let back: Item = serde_json::from_value(serde_json::to_value(&turn).unwrap()).unwrap();
1498 assert_eq!(back, turn);
1499 }
1500 }
1501}