Skip to main content

escouade_plugin_protocol/
engine.rs

1//! The contract of a harness in process (INT-9, docs/PLUGINS.md §4.5 and §4.6): what the core and the plugin of a
2//! harness say to each other. The core launches the harness itself, from the description the plugin gives
3//! (`engine/launch`), after it checked it (the fence of private projects, read-only, the token of Escouade's server
4//! never among the arguments, the files written in the agent's own folder only). What the harness says comes to the
5//! plugin in batches of raw lines (`agent/frames`), which it translates into normalized items and effects (`Item`)
6//! that the core applies to the agent. What the plugin has to write to the harness comes back in its answers
7//! (`writes`), and the core writes it on the harness's input. The PTY of a terminal view never goes through the
8//! plugin: only its launch (`terminal/launch`) and its hooks (`terminal/hook`), both ways.
9//!
10//! Every type tolerates what it does not know: a field added later is left aside, an item of an unknown `op` is
11//! `Item::Unknown`, which the core ignores.
12
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16/// The methods of the contract, all called by the core.
17pub mod method {
18    /// Stateless: the description of a harness's launch for an agent (`LaunchParams` → `Launch`).
19    pub const LAUNCH: &str = "engine/launch";
20    /// Stateless: the launch of a one-shot question (`OneShotParams` → `OneShotLaunch`).
21    pub const ONE_SHOT: &str = "engine/oneShot";
22    /// Stateless: the answer of a one-shot question, from what its process wrote (`OneShotOutput` → `OneShotText`).
23    pub const ONE_SHOT_READ: &str = "engine/oneShotRead";
24    /// An agent given to the plugin, before its launch and again after a restart (`Attach`).
25    pub const ATTACH: &str = "agent/attach";
26    /// The agent's process is gone: its state may be dropped (a notification, `AgentRef`).
27    pub const DETACH: &str = "agent/detach";
28    /// What the harness wrote, in a batch of raw lines (`Frames` → `Items`).
29    pub const FRAMES: &str = "agent/frames";
30    /// A message to send (`SendMessage` → `Writes`).
31    pub const SEND_MESSAGE: &str = "agent/sendMessage";
32    /// Stop the running turn (`Interrupt` → `Writes`).
33    pub const INTERRUPT: &str = "agent/interrupt";
34    /// A change of model, effort or mode (`SetValue` → `Writes`).
35    pub const SET_MODEL: &str = "agent/setModel";
36    pub const SET_EFFORT: &str = "agent/setEffort";
37    pub const SET_MODE: &str = "agent/setMode";
38    /// The answer to a permission or a question the harness asked (`AnswerPermission`, `AnswerQuestion` → `Writes`).
39    pub const ANSWER_PERMISSION: &str = "agent/answerPermission";
40    pub const ANSWER_QUESTION: &str = "agent/answerQuestion";
41    /// The quota of the agent's account, as its process reads it (`AgentRef` → `Usage`).
42    pub const READ_USAGE: &str = "agent/readUsage";
43    /// The launch of the harness's own interface in a terminal (`TerminalParams` → `Launch`).
44    pub const TERMINAL_LAUNCH: &str = "terminal/launch";
45    /// One of its hooks, with what the core answers it (`Hook` → `HookAnswer`).
46    pub const TERMINAL_HOOK: &str = "terminal/hook";
47    /// The lines the core read of what the harness's own interface writes, and its notes (`ViewParams` → `Items`).
48    pub const VIEW_TRANSLATE: &str = "view/translate";
49    /// A harness shared by agents (`Launch::shared`): the agent's session opened on it (`Open` → `Opened`).
50    pub const OPEN: &str = "agent/open";
51    /// The agent stops using its shared harness (`Close`, a call or a notification): gently, its turn finished first and
52    /// `Item::Closed` said once it is; or at once, its turn stopped.
53    pub const CLOSE: &str = "agent/close";
54    /// The agent was the last in its folder: its shared harness lets that folder go (`AgentRef`).
55    pub const LET_GO: &str = "agent/letGo";
56    /// Compact its context (`Compact` → `Compacted`), as `Engine::compaction` says it can.
57    pub const COMPACT: &str = "agent/compact";
58    /// Give its session a title (`SetValue`).
59    pub const RENAME: &str = "agent/rename";
60    /// A request in the harness's own protocol, its answer: for the tests, and what has no method yet (`Control`).
61    pub const CONTROL: &str = "agent/control";
62    /// From the plugin, without being asked (a notification, `Pushed`): what its harness said by a way only the plugin
63    /// reads (the event stream of a shared server).
64    pub const ITEMS: &str = "agent/items";
65    /// Its program ended by itself, for a harness that declares `relaunches` (`Gone` → `Items`): `Item::Relaunch` goes
66    /// on with another, `Item::Stop` says why not, nothing ends the agent's process.
67    pub const PROGRAM_GONE: &str = "agent/programGone";
68    /// Stop one of its background tasks (`Task` → `Asked`).
69    pub const STOP_TASK: &str = "agent/stopTask";
70    /// What a background task wrote so far (`Task` → `Asked`).
71    pub const TASK_OUTPUT: &str = "agent/taskOutput";
72    /// Link its session to its vendor's site, or end the link (`RemoteLink` → `Asked`).
73    pub const REMOTE: &str = "agent/remote";
74}
75
76/// How many bytes of lines a batch of `agent/frames` carries at most. A batch is what was read while the last one
77/// was with the plugin: none waits for more lines, a lone line goes at once.
78pub const BATCH_BYTES: usize = 64 * 1024;
79
80/// Where a conversation starts from.
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
82#[serde(tag = "kind", rename_all = "camelCase")]
83pub enum StartAt {
84    #[default]
85    New,
86    Resume {
87        session: String,
88    },
89    Fork {
90        of: String,
91        #[serde(default)]
92        at: Option<String>,
93    },
94}
95
96/// Escouade's MCP server given to the agent. The plugin may put the token in a variable or a file of the agent's own,
97/// never among the arguments: the core refuses such a launch.
98#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct Grant {
100    pub url: String,
101    pub token: String,
102}
103
104impl std::fmt::Debug for Grant {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.debug_struct("Grant")
107            .field("url", &self.url)
108            .field("token", &"…")
109            .finish()
110    }
111}
112
113/// What `engine/launch` is told.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
115#[serde(rename_all = "camelCase")]
116pub struct LaunchParams {
117    /// The key of the agent's process, as `agent/*` names it.
118    pub agent: String,
119    pub cwd: String,
120    pub model: String,
121    #[serde(default)]
122    pub effort: String,
123    pub mode: String,
124    #[serde(default)]
125    pub append_prompt: Option<String>,
126    /// What was said in the harness's own interface since the agent last ran (« Vue Cursor », a relay of the core's):
127    /// it goes before the next message.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub relay: Option<String>,
130    /// Its account goes by its key (`HarnessDecl::key_var` given), not by the machine's sign-in: what a refusal says.
131    #[serde(default)]
132    pub by_key: bool,
133    #[serde(default)]
134    pub start: StartAt,
135    #[serde(default)]
136    pub read_dirs: Vec<String>,
137    /// Escouade's server, None when refused.
138    #[serde(default)]
139    pub escouade: Option<Grant>,
140    /// The paths refused to its tools (« Projet privé »), as its program spells them: each must be in the launch,
141    /// and `Launch::fence_applied` true, or the core does not launch it.
142    #[serde(default)]
143    pub fence: Vec<String>,
144    /// A discussion of the Docs tab: it changes no file and runs no command. `Launch::read_only` must say it holds.
145    #[serde(default)]
146    pub read_only: bool,
147    /// The private file the core made for Escouade's server (`Engine::grant_file`), which a plugin's launch may name.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub escouade_file: Option<String>,
150    /// Its program, as the user knows it.
151    #[serde(default)]
152    pub program: String,
153    /// The folder of the agent's own, where the files of the launch may be written; None: no file may be.
154    #[serde(default)]
155    pub home: Option<String>,
156    /// For a plugin that serves the harnesses other plugins describe (`dev.escouade.acp`, PLG-6): the harness as its
157    /// manifest declares it (`HarnessDecl`, its `acp` included). None for a plugin's own harness.
158    #[serde(default)]
159    pub described: Option<HarnessDecl>,
160    /// The language of the app (`fr`, `en`), for the lines the plugin writes in the conversation.
161    #[serde(default)]
162    pub lang: String,
163    /// Whether its model takes an effort, as its account's harness told it; None: the plugin's own rule.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub takes_effort: Option<bool>,
166    /// A settings file the core wrote for it (`Engine::fence_settings`): it holds the fence, and a launch that names
167    /// it needs not spell the paths (`fence`).
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub settings: Option<String>,
170    /// The file the core wrote to keep Escouade's server out of it (`Engine::refusal`), when it is refused.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub refusal_file: Option<String>,
173}
174
175/// A variable of a launch.
176#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub struct EnvVar {
178    pub name: String,
179    pub value: String,
180}
181
182impl std::fmt::Debug for EnvVar {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        write!(f, "{}=…", self.name)
185    }
186}
187
188/// A file the core writes before the launch, in the agent's own folder.
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub struct FileToWrite {
191    pub path: String,
192    pub text: String,
193}
194
195/// The description of a launch (`engine/launch`, `terminal/launch`): never its program, which the core found itself
196/// (`locate`), and runs in its Job object, under WSL when need be.
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
198#[serde(rename_all = "camelCase")]
199pub struct Launch {
200    #[serde(default)]
201    pub args: Vec<String>,
202    #[serde(default)]
203    pub env: Vec<EnvVar>,
204    /// The variables of the app's own environment it must not get (`MSYSTEM` for Cursor).
205    #[serde(default)]
206    pub remove_env: Vec<String>,
207    #[serde(default)]
208    pub files: Vec<FileToWrite>,
209    /// The fence of `LaunchParams::fence` is in the launch (its arguments, variables or files).
210    #[serde(default)]
211    pub fence_applied: bool,
212    /// The harness is held read-only by this launch.
213    #[serde(default)]
214    pub read_only: bool,
215    /// The fence of `LaunchParams::fence` is held by the plugin itself, on the reads and writes the harness makes
216    /// through it (ACP's `fs/read_text_file`, `fs/write_text_file` and the permissions it asks), not by the launch:
217    /// the core then takes the launch without the paths in it (PLG-6).
218    #[serde(default)]
219    pub fence_by_client: bool,
220    /// The first lines written on its input, once it started.
221    #[serde(default)]
222    pub writes: Vec<String>,
223    /// One process for several agents (`opencode serve`, 0,7 Go): the core runs it for those of the same program and
224    /// variables, and does not start it again for the next; the plugin speaks to it by a way of its own.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub shared: Option<SharedLaunch>,
227    /// For a terminal view: the trust bands its interface asks, shown before it opens.
228    #[serde(default)]
229    pub trust: Vec<String>,
230}
231
232/// How a process shared by agents starts and is shared. It runs in a folder of its own, none of the agents': a worktree
233/// removed would find it held. It lives from its first agent to its last, in the core's Job object.
234#[derive(Clone, PartialEq, Serialize, Deserialize, Default)]
235#[serde(rename_all = "camelCase")]
236pub struct SharedLaunch {
237    /// What the line that says it is ready holds: the line goes to the plugin (`SharedRef::line`).
238    pub ready: String,
239    /// The template of the line where the server says its address (`http://{host}:{port}`): the port the core reads
240    /// there is the one `local:shared` lets the plugin reach (`{host}` is `127.0.0.1` and nothing else, ESC-332).
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub listens: Option<String>,
243    /// A folder has one agent per process: another agent in that folder gets a process of its own.
244    #[serde(default)]
245    pub one_agent_a_folder: bool,
246    /// What the plugin wants back with each agent on that process (its password): kept by the core, never read.
247    #[serde(default)]
248    pub keep: Value,
249}
250
251impl std::fmt::Debug for SharedLaunch {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        f.debug_struct("SharedLaunch")
254            .field("ready", &self.ready)
255            .field("listens", &self.listens)
256            .field("one_agent_a_folder", &self.one_agent_a_folder)
257            .field("keep", &"…")
258            .finish()
259    }
260}
261
262/// The shared process an agent is on: enough for a plugin that started again to find it.
263#[derive(Clone, PartialEq, Serialize, Deserialize, Default)]
264pub struct SharedRef {
265    /// Which process, the same for each of its agents.
266    pub key: String,
267    /// The line that said it is ready.
268    pub line: String,
269    /// `SharedLaunch::keep`, as given by the launch that started it.
270    #[serde(default)]
271    pub keep: Value,
272}
273
274impl std::fmt::Debug for SharedRef {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        f.debug_struct("SharedRef")
277            .field("key", &self.key)
278            .field("line", &self.line)
279            .field("keep", &"…")
280            .finish()
281    }
282}
283
284/// An agent given to the plugin: what it cannot know by itself, given again after each restart and each change.
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
286#[serde(rename_all = "camelCase")]
287pub struct Attach {
288    pub agent: String,
289    pub cwd: String,
290    #[serde(default)]
291    pub session: Option<String>,
292    #[serde(default)]
293    pub model: String,
294    #[serde(default)]
295    pub effort: String,
296    #[serde(default)]
297    pub mode: String,
298    /// The requests of the harness waiting for the user's answer, by their ids.
299    #[serde(default)]
300    pub pending: Vec<String>,
301    /// As `LaunchParams::described`.
302    #[serde(default)]
303    pub described: Option<HarnessDecl>,
304    /// As `LaunchParams::lang`.
305    #[serde(default)]
306    pub lang: String,
307    /// The shared process it is on (`Launch::shared`), once it runs.
308    #[serde(default, skip_serializing_if = "Option::is_none")]
309    pub shared: Option<SharedRef>,
310    /// Its folders the plugin declares (`permissions.files`), by root (`agent`, `agentRepo`, `agentHome`, `temp`), resolved
311    /// by the core, for `fs/*`.
312    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
313    pub roots: std::collections::BTreeMap<String, String>,
314    /// As `LaunchParams::fence`, `read_only` and `append_prompt`, for a plugin that holds them itself, or starts again:
315    /// on a session of its own (`Launch::shared`), in rules it writes.
316    #[serde(default, skip_serializing_if = "Vec::is_empty")]
317    pub fence: Vec<String>,
318    #[serde(default)]
319    pub read_only: bool,
320    #[serde(default, skip_serializing_if = "Option::is_none")]
321    pub append_prompt: Option<String>,
322    /// A turn of its runs, as far as the core knows: one that ended while the plugin was down is told so.
323    #[serde(default)]
324    pub running: bool,
325    /// As `LaunchParams::home`, `read_dirs` and `by_key`, for a plugin that starts again and writes in that folder (its
326    /// rules, a harness that `relaunches`).
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub home: Option<String>,
329    #[serde(default, skip_serializing_if = "Vec::is_empty")]
330    pub read_dirs: Vec<String>,
331    #[serde(default)]
332    pub by_key: bool,
333}
334
335/// What `agent/open` is told: the agent's session, on the shared process of its `agent/attach`.
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
337pub struct Open {
338    pub agent: String,
339    #[serde(default)]
340    pub start: StartAt,
341    /// Escouade's server, None when refused.
342    #[serde(default)]
343    pub escouade: Option<Grant>,
344}
345
346/// What `agent/open` found.
347#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
348pub struct Opened {
349    /// How its harness's interface joins it in a terminal, what is secret in a variable.
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub attach: Option<Launch>,
352    /// The models of its account, as `ToldModel`s of the core.
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub catalog: Option<Value>,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358pub struct Close {
359    pub agent: String,
360    /// Its turn stopped first, not waited for.
361    #[serde(default)]
362    pub now: bool,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
366pub struct Compact {
367    pub agent: String,
368    #[serde(default)]
369    pub instructions: String,
370}
371
372/// `EngineProcess::compact`'s answer: it is over; false, it runs as a turn the harness ends itself.
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
374pub struct Compacted {
375    #[serde(default)]
376    pub over: bool,
377    /// What the core writes to the harness to ask it: `Item::Compacted` then says the end.
378    #[serde(default, skip_serializing_if = "Vec::is_empty")]
379    pub writes: Vec<String>,
380    /// Written, it runs as a turn the harness ends itself (Codex): no `Item::Compacted` to wait for.
381    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
382    pub turn: bool,
383}
384
385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct Control {
388    pub agent: String,
389    pub request: Value,
390    /// How long the answer may take, in milliseconds.
391    #[serde(default)]
392    pub within_ms: u64,
393}
394
395/// `method::ITEMS`.
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397pub struct Pushed {
398    pub agent: String,
399    pub items: Vec<Item>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
403pub struct AgentRef {
404    pub agent: String,
405}
406
407/// What `agent/interrupt` is told (a plugin may read it as an `AgentRef`).
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct Interrupt {
411    pub agent: String,
412    /// The messages queued after the turn go too.
413    #[serde(default)]
414    pub cancel_queued: bool,
415}
416
417/// `method::PROGRAM_GONE`: how it ended, and what it said last on its error output.
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct Gone {
420    pub agent: String,
421    #[serde(default)]
422    pub code: Option<i32>,
423    #[serde(default)]
424    pub said: String,
425}
426
427/// A batch of what the harness wrote: each line as it read it, never read again by the core.
428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429pub struct Frames {
430    pub agent: String,
431    pub frames: Vec<String>,
432}
433
434/// What a batch made. The answer to any call that answers `Writes` may be this instead: its items applied, in their
435/// order (a message that starts a program again: `Item::Relaunch`, then its `Item::Write`).
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
437pub struct Items {
438    #[serde(default)]
439    pub items: Vec<Item>,
440}
441
442/// What the plugin writes to the harness, line by line.
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
444pub struct Writes {
445    #[serde(default)]
446    pub writes: Vec<String>,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
450pub struct SendMessage {
451    pub agent: String,
452    pub uuid: String,
453    pub text: String,
454    /// The message as its harness takes it, when the core made it (ACP's `prompt` blocks: its images and its text
455    /// files, INT-10.1); None: `text` alone.
456    #[serde(default, skip_serializing_if = "Option::is_none")]
457    pub content: Option<serde_json::Value>,
458}
459
460#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
461pub struct SetValue {
462    pub agent: String,
463    pub value: String,
464}
465
466#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct AnswerPermission {
469    pub agent: String,
470    pub request: String,
471    pub allow: bool,
472    /// Allowed for the rest of the session.
473    #[serde(default)]
474    pub always: bool,
475    /// The tool's input, maybe changed, when allowed.
476    #[serde(default)]
477    pub input: Value,
478    /// What the agent is told, when refused.
479    #[serde(default)]
480    pub message: Option<String>,
481    /// A plan approved: the mode its work goes on in (the one the agent was in before Plan).
482    #[serde(default)]
483    pub resume: Option<String>,
484    /// The call it answers (`Item::Ask::call`).
485    #[serde(default, skip_serializing_if = "String::is_empty")]
486    pub call: String,
487    /// « Toujours »: the rules of `Item::Ask::suggestions` it goes by.
488    #[serde(default, skip_serializing_if = "Value::is_null")]
489    pub rules: Value,
490}
491
492#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493pub struct AnswerQuestion {
494    pub agent: String,
495    pub request: String,
496    pub call: String,
497    /// The answers, by question.
498    pub answers: Value,
499    /// The questions they answer, as the card has them, for a harness that wants them in its order.
500    #[serde(default)]
501    pub questions: Value,
502    /// The call's input with its answers, for a harness that takes it whole.
503    #[serde(default, skip_serializing_if = "Value::is_null")]
504    pub input: Value,
505}
506
507/// The quota its process read (`usage::parse_windows` reads its windows); null when none.
508#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
509pub struct Usage {
510    #[serde(default)]
511    pub usage: Value,
512}
513
514/// The answer of `agent/readUsage` when the quota is to be asked of the harness: `writes` go to its input, and
515/// `Item::Usage` brings what it said.
516#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
517pub struct UsageAsked {
518    #[serde(default)]
519    pub writes: Vec<String>,
520}
521
522/// The answer of a call whose end is the harness's to tell (`agent/interrupt`, `agent/setModel`, `agent/control`…): `writes` go
523/// to its input, and `Item::Answer` under `ask` brings what it said. An answer without `ask` is over as it is.
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
525pub struct Asked {
526    #[serde(default)]
527    pub writes: Vec<String>,
528    #[serde(default)]
529    pub ask: String,
530}
531
532/// One of its background tasks.
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
534pub struct Task {
535    pub agent: String,
536    pub task: String,
537}
538
539/// `method::REMOTE`: its session linked (`name` on the vendor's site, kept when it ends if `keep`, joining the session
540/// `reattach`), or unlinked.
541#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(rename_all = "camelCase")]
543pub struct RemoteLink {
544    pub agent: String,
545    pub enabled: bool,
546    #[serde(default)]
547    pub name: String,
548    #[serde(default)]
549    pub keep: bool,
550    #[serde(default, skip_serializing_if = "Option::is_none")]
551    pub reattach: Option<String>,
552}
553
554/// What a model cost in the turn that ends (`Item::Result`).
555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
556pub struct ModelTurn {
557    pub model: String,
558    #[serde(default)]
559    pub input: u64,
560    #[serde(default)]
561    pub output: u64,
562    #[serde(default)]
563    pub cache: u64,
564    #[serde(default)]
565    pub cost: f64,
566}
567
568/// The tokens and cost of a turn.
569#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
570pub struct TurnUsage {
571    #[serde(default)]
572    pub input: u64,
573    #[serde(default)]
574    pub output: u64,
575    #[serde(default)]
576    pub cache: u64,
577    #[serde(default)]
578    pub cost: f64,
579}
580
581/// A normalized item or an effect, which the core applies to the agent: the same as the conversation's (`user`,
582/// `text`, `thinking`, `tool`, `question`, `permission`, `turn`, `notice`) and the effects (`Effects` of
583/// `agent.rs`).
584#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
585#[serde(tag = "op", rename_all = "camelCase")]
586pub enum Item {
587    /// A turn begins, in the session `session` (the agent's from now on).
588    Start {
589        #[serde(default)]
590        session: Option<String>,
591    },
592    /// The agent's session, opened (new, resumed or copied) before any turn: the agent's from now on, its status left
593    /// as it is (INT-10.6: a copy of Mistral Vibe's has its own session as soon as its process starts).
594    Session { session: String },
595    /// Its text; a subagent's under `parent`. With `id`, the whole of the text streamed under that id (`Delta`).
596    #[serde(rename_all = "camelCase")]
597    Text {
598        text: String,
599        #[serde(default)]
600        parent: Option<String>,
601        #[serde(default)]
602        id: Option<String>,
603    },
604    Thinking {
605        text: String,
606        #[serde(default)]
607        parent: Option<String>,
608        #[serde(default)]
609        id: Option<String>,
610    },
611    /// A piece of a text (or a reasoning) as it is written, under `id`: the first one makes its item, the others add to
612    /// it; `Text` (`Thinking`) of the same id ends it.
613    Delta {
614        id: String,
615        text: String,
616        #[serde(default)]
617        thinking: bool,
618        #[serde(default)]
619        parent: Option<String>,
620    },
621    /// The model a subagent runs on, under its call.
622    Subagent { call: String, model: String },
623    /// A tool called, named as Escouade names them (Claude Code's names: `Bash`, `Edit`, `Task`…).
624    Tool {
625        id: String,
626        name: String,
627        #[serde(default)]
628        input: Value,
629        #[serde(default)]
630        parent: Option<String>,
631        /// Not a row of the plan: its task list comes by `Todos`.
632        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
633        unplanned: bool,
634    },
635    /// What is known later of a call: its input (an edit's file), or the name it turns out to have (`Write` for a
636    /// file added).
637    ToolInput {
638        id: String,
639        #[serde(default)]
640        name: Option<String>,
641        #[serde(default)]
642        input: Value,
643    },
644    /// Its result; `result` the structured one Claude Code gives (`{ filePath, structuredPatch }` of an edit,
645    /// `{ stdout, stderr }` of a command), which shows its diff.
646    ToolDone {
647        id: String,
648        #[serde(default)]
649        output: String,
650        #[serde(default)]
651        error: bool,
652        #[serde(default)]
653        result: Value,
654    },
655    /// A file it changed (the effect `files_changed`).
656    Touched { path: String },
657    /// The approval of the call `call`, asked as the request `request`: the call's tool and input unless `tool`
658    /// says another (`ExitPlanMode` and its `plan`), what the card says of it (`description`, `reason`), the
659    /// write it would make (`diff`, shown on the card), and whether « Pour cet agent » is offered.
660    #[serde(rename_all = "camelCase")]
661    Permission {
662        request: String,
663        call: String,
664        #[serde(default)]
665        tool: Option<String>,
666        #[serde(default)]
667        input: Option<Value>,
668        #[serde(default)]
669        description: Option<String>,
670        #[serde(default)]
671        reason: Option<String>,
672        #[serde(default)]
673        diff: Option<Diff>,
674        #[serde(default)]
675        can_always: bool,
676        /// The call of the subagent that asks it.
677        #[serde(default)]
678        parent: Option<String>,
679        /// The hunks of a patch the harness gave as they are, in place of `diff` (`hunks_of`).
680        #[serde(default, skip_serializing_if = "Option::is_none")]
681        hunks: Option<Value>,
682        /// « Always » is the harness's, for its session: not worded as « Pour cet agent ».
683        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
684        always_by_harness: bool,
685        /// The turn stays open on this card, what was streaming closed (a plan to approve at the end of a turn).
686        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
687        ends_stream: bool,
688    },
689    /// Questions to the user (each `{question, header, multiSelect, options: [{label, description}]}`).
690    Question {
691        request: String,
692        call: String,
693        questions: Value,
694        /// The call of the subagent that asks them.
695        #[serde(default, skip_serializing_if = "Option::is_none")]
696        parent: Option<String>,
697    },
698    /// A request answered in the harness's own interface: its card says so and goes. `decision` of a permission
699    /// (`allow`, `always`, `deny`), `answers` of questions (for each in its order, the labels chosen), else cancelled.
700    Answered {
701        request: String,
702        #[serde(default, skip_serializing_if = "Option::is_none")]
703        decision: Option<String>,
704        #[serde(default, skip_serializing_if = "Option::is_none")]
705        answers: Option<Value>,
706    },
707    /// The user's message typed in the harness's own interface: shown as they typed it.
708    User { id: String, text: String },
709    /// What the user typed in the harness's own interface (or Escouade pasted there), read in its view: their message, under
710    /// `id` when it has one, and a turn starts. `prompt`: the id of its prompt, when the interface writes one message
711    /// twice (a command, as typed then as `<command-name>`).
712    Typed {
713        #[serde(default)]
714        id: Option<String>,
715        text: String,
716        #[serde(default)]
717        images: u32,
718        #[serde(default, skip_serializing_if = "Option::is_none")]
719        prompt: Option<String>,
720    },
721    /// The view's file was written since its interface started: the interface is up.
722    Spoke,
723    /// Something of its session is written: its absence now would be a loss.
724    Wrote,
725    /// The interface waits for the user, for this.
726    Waits { body: String },
727    /// The same, for the call of this tool, which the core words.
728    WaitsOn {
729        tool: String,
730        #[serde(default)]
731        input: Value,
732    },
733    /// What the interface is about to ask, which it waits for once a hook says so.
734    Asks {
735        tool: String,
736        #[serde(default)]
737        input: Value,
738    },
739    /// The agent works: a turn runs from now on, if none did.
740    Runs,
741    /// What it waited for is answered: it works again.
742    Goes,
743    /// The turn of its harness under way (the end the core makes after a hook is only that turn's).
744    ViewTurn {
745        #[serde(default)]
746        id: Option<String>,
747    },
748    /// The view's turn is over: the core closes it, and gives the note `end` back with what it knows of it.
749    ViewEnd {
750        #[serde(default)]
751        interrupted: bool,
752        /// Its length, in ms.
753        #[serde(default, skip_serializing_if = "Option::is_none")]
754        duration: Option<u64>,
755        #[serde(default, skip_serializing_if = "Option::is_none")]
756        error: Option<String>,
757    },
758    /// The usage of a message of the view, as its API gave it, which the core prices at the end of the turn.
759    ViewUsage {
760        id: String,
761        model: String,
762        #[serde(default)]
763        usage: Value,
764    },
765    /// What went wrong in the view's turn, for its end.
766    ViewError { error: String },
767    /// A tool of the view whose run is not told, shown as `status`: again under the same id, its status changes.
768    Row {
769        #[serde(default)]
770        id: Option<String>,
771        name: String,
772        #[serde(default)]
773        input: Value,
774        status: String,
775    },
776    /// The interface tells how the link of its session to claude.ai went: at `url`, or, with none, that it did not open.
777    Bridged {
778        #[serde(default)]
779        url: Option<String>,
780        #[serde(default)]
781        said: String,
782    },
783    /// A sign that the interface's session is linked to claude.ai.
784    Linked,
785    /// The model the harness took in place of the agent's (one its account does not offer).
786    Model { model: String },
787    /// What the agent's context holds (`tokens`), and the window of its model.
788    Context {
789        #[serde(default, skip_serializing_if = "Option::is_none")]
790        tokens: Option<u64>,
791        #[serde(default, skip_serializing_if = "Option::is_none")]
792        window: Option<u64>,
793    },
794    /// The task list of the agent, as Claude Code's `TodoWrite` gives it (`[{ content, status, activeForm }]`),
795    /// for its plan.
796    Todos { todos: Value },
797    /// The harness changed its mode by itself (out of its mode Plan): Escouade's mode it is in now.
798    Mode { mode: String },
799    /// The turn is over.
800    Turn {
801        #[serde(default)]
802        interrupted: bool,
803        /// Stopped by the usage limit, which resets then (ms), if told: `null` for a limit whose reset is not told,
804        /// left out for none.
805        #[serde(
806            default,
807            deserialize_with = "present",
808            skip_serializing_if = "Option::is_none"
809        )]
810        limited: Option<Option<i64>>,
811        #[serde(default)]
812        usage: TurnUsage,
813        /// It ended in error, and why (a model refused, a key refused).
814        #[serde(default)]
815        error: Option<String>,
816        /// The last entry of its session: where a copy forks it.
817        #[serde(default, skip_serializing_if = "Option::is_none")]
818        entry: Option<String>,
819    },
820    /// A line of the conversation from Escouade or the harness, not the model (`info`, `warning`, `error`).
821    Notice {
822        text: String,
823        #[serde(default = "info")]
824        level: String,
825    },
826    /// A line for the harness's input, written by the core in the order of the items.
827    Write { line: String },
828    /// The approval of a call, its card written whole: the tool asked, what it says of it, the hunks of the write it
829    /// would make (Claude Code's `structuredPatch`), how far « Toujours » goes (`alwaysScope`, left out for a plan).
830    #[serde(rename_all = "camelCase")]
831    PermissionCard {
832        request: String,
833        call: String,
834        tool: String,
835        #[serde(default)]
836        input: Value,
837        #[serde(default)]
838        description: Option<String>,
839        #[serde(default)]
840        reason: Option<String>,
841        #[serde(default)]
842        hunks: Value,
843        #[serde(default)]
844        can_always: bool,
845        #[serde(default)]
846        always_scope: Option<String>,
847        #[serde(default)]
848        parent: Option<String>,
849    },
850    /// Questions asked by a subagent (`parent`), or a form (`form`: what it is for, `{ message, source }`).
851    QuestionCard {
852        request: String,
853        call: String,
854        questions: Value,
855        #[serde(default)]
856        form: Value,
857        #[serde(default)]
858        parent: Option<String>,
859    },
860    /// The account's quota as a call told it: its meters (`usage::parse_windows`'s).
861    Quota {
862        #[serde(default)]
863        meters: Value,
864    },
865    /// What `agent/readUsage` asked of the harness (`UsageAsked`), as `Usage::usage`.
866    Usage {
867        #[serde(default)]
868        usage: Value,
869    },
870    /// The turn ending next stopped at a limit it waits for: told in its turn, the agent at rest and not in error.
871    LimitRest,
872    /// A harness that declares `opens`: its session is open and its first message may go; `models` are the account's
873    /// (`ToldModel`s), for the menu.
874    Ready {
875        #[serde(default)]
876        models: Value,
877    },
878    /// The end of what `agent/compact` asked, and why it failed.
879    Compacted {
880        #[serde(default)]
881        error: Option<String>,
882    },
883    /// The harness is of no use: the core stops it, and `reason` is what its end says (its protocol, no sign-in).
884    Stop { reason: String },
885    /// The last entry of its session a copy may start after, when no `Turn` tells it (a plan to approve).
886    Entry {
887        #[serde(default)]
888        entry: Option<String>,
889    },
890    /// What the agent is doing, shown on its row, when the item that follows does not tell it: a command that only
891    /// reads a file is a `Read` (`tool`, `input`), a reasoning begun says so before it has a word (`block`: `thinking`).
892    Activity {
893        #[serde(default)]
894        tool: Option<String>,
895        #[serde(default)]
896        input: Value,
897        #[serde(default)]
898        block: Option<String>,
899    },
900    /// How long the turn that ends took, as its harness counted it (ms), told just before its `Turn`; left out, the core
901    /// counts it.
902    Took { ms: i64 },
903    /// A harness that `relaunches`: its program stopped, and another started from this description, checked as the
904    /// first one was; the lines written after it go to the new one.
905    Relaunch { launch: Box<Launch> },
906    /// A harness that `relaunches`: its program stopped, the agent kept; `Relaunch` starts the next one.
907    Pause,
908    /// A file written now, in the agent's own folder alone, checked as those of a launch: rules read while it runs.
909    File { path: String, text: String },
910    /// Asked to stop gently (`method::CLOSE`), its turn is over: the agent has left its shared process.
911    Closed,
912    /// The conversation it runs (a harness that says so at each start): another than the agent's starts anew, its plan
913    /// forgotten; `notice` is what the agent's conversation says when that happens to a process that had one.
914    Conversation {
915        session: String,
916        #[serde(default)]
917        notice: String,
918    },
919    /// A message of the model begins, under `id`: what it reads (`usage`, as the API tells it) counts in the running
920    /// turn until `Item::Result`; `main`: of the agent itself, not of a subagent.
921    Message {
922        id: String,
923        #[serde(default)]
924        model: String,
925        #[serde(default)]
926        usage: Value,
927        #[serde(default)]
928        main: bool,
929    },
930    /// What the message `id` wrote so far, in tokens.
931    MessageOutput { id: String, output: u64 },
932    /// What the harness says of its background tasks, for the plan (`task_started`, `task_progress`…).
933    Tasks { frame: Value },
934    /// A line of the conversation that comes from neither the user nor the model (a task that ended, a message of
935    /// another agent): `source` says which.
936    Event {
937        #[serde(default)]
938        id: Option<String>,
939        source: String,
940        text: String,
941        #[serde(default, skip_serializing_if = "Option::is_none")]
942        from: Option<String>,
943    },
944    /// What a local command printed (`output`, `error`).
945    Output {
946        #[serde(default)]
947        id: Option<String>,
948        level: String,
949        text: String,
950    },
951    /// The whole call of a tool, once its message is whole: `Tool` began it if it was streamed, its row is then told
952    /// its input; the plan hears of it either way.
953    Call {
954        id: String,
955        name: String,
956        #[serde(default)]
957        input: Value,
958        #[serde(default)]
959        parent: Option<String>,
960    },
961    /// Its result, whatever the row says by now (`ToolDone` leaves a row that ended as it is).
962    ToolResult {
963        id: String,
964        #[serde(default)]
965        output: String,
966        #[serde(default)]
967        error: bool,
968        #[serde(default)]
969        result: Value,
970    },
971    /// A user's message the harness says back: the echo of one sent by the core, or one sent another way. `origin` is
972    /// who it comes from when no person wrote it (`task-notification`, `peer`…), `from` the agent that sent it.
973    Replayed {
974        #[serde(default)]
975        id: Option<String>,
976        #[serde(default)]
977        text: String,
978        #[serde(default, skip_serializing_if = "Option::is_none")]
979        origin: Option<String>,
980        #[serde(default, skip_serializing_if = "Option::is_none")]
981        from: Option<String>,
982        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
983        synthetic: bool,
984        #[serde(default)]
985        images: u32,
986        #[serde(default, skip_serializing_if = "Vec::is_empty")]
987        files: Vec<String>,
988    },
989    /// The harness asks leave for a call, or asks the user (`tool` `AskUserQuestion`): `suggestions` are the rules
990    /// « Toujours » would keep, given back in `AnswerPermission::rules`; `task` the subagent that asks. The core answers
991    /// by itself a read of a file the user joined.
992    #[serde(rename_all = "camelCase")]
993    Ask {
994        request: String,
995        call: String,
996        tool: String,
997        #[serde(default)]
998        input: Value,
999        #[serde(default)]
1000        title: Option<String>,
1001        #[serde(default)]
1002        description: Option<String>,
1003        #[serde(default)]
1004        reason: Option<String>,
1005        #[serde(default)]
1006        suggestions: Value,
1007        #[serde(default)]
1008        no_always: bool,
1009        #[serde(default)]
1010        default_no: bool,
1011        #[serde(default)]
1012        task: Option<String>,
1013    },
1014    /// The end of a turn the harness counts itself: what each model cost, the context `window` of the one that ran,
1015    /// whether it failed and why. `limited`: stopped by the usage limit, which resets at `resets` (ms) if told;
1016    /// `sign_in`: by a sign-in of its account that could not be refreshed; `failure` is what a failure says, `reply`
1017    /// what a good end says when the model wrote nothing.
1018    #[serde(rename_all = "camelCase")]
1019    Result {
1020        #[serde(default)]
1021        id: Option<String>,
1022        #[serde(default)]
1023        failed: bool,
1024        #[serde(default)]
1025        error: Option<String>,
1026        #[serde(default)]
1027        failure: String,
1028        #[serde(default)]
1029        reply: String,
1030        #[serde(default)]
1031        limited: bool,
1032        #[serde(default)]
1033        resets: Option<i64>,
1034        #[serde(default)]
1035        sign_in: bool,
1036        #[serde(default)]
1037        duration: Value,
1038        #[serde(default)]
1039        window: Option<u64>,
1040        #[serde(default)]
1041        cost: f64,
1042        #[serde(default)]
1043        tokens: u64,
1044        #[serde(default)]
1045        models: Vec<ModelTurn>,
1046    },
1047    /// A usage limit was met: `scoped` to the model it runs on, not to its account.
1048    Limit {
1049        #[serde(default)]
1050        scoped: bool,
1051    },
1052    /// Whether its account answers as extra usage.
1053    Overage { on: bool },
1054    /// The state of its link to its vendor's site (`ready`, `connected`…).
1055    Remote {
1056        #[serde(default)]
1057        state: Option<String>,
1058    },
1059    /// What its harness said when it started: its commands and its models (as it tells them), which `Ready` follows.
1060    Told {
1061        #[serde(default)]
1062        commands: Value,
1063        #[serde(default)]
1064        models: Value,
1065    },
1066    /// What the harness said of the call `ask` (`Asked`): its answer, or why it refused.
1067    Answer {
1068        ask: String,
1069        #[serde(default)]
1070        answer: Value,
1071        #[serde(default, skip_serializing_if = "Option::is_none")]
1072        error: Option<String>,
1073    },
1074    /// What the harness wrote that is none of its protocol, kept for what its end says.
1075    Said { line: String },
1076    /// An item of a later version of the contract: left aside.
1077    #[serde(other)]
1078    Unknown,
1079}
1080
1081fn info() -> String {
1082    "info".into()
1083}
1084
1085/// A field that is there, `null` included (`Some(None)`); left out, it is `None` (`default`).
1086fn present<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<Option<i64>>, D::Error> {
1087    Ok(Some(Option::<i64>::deserialize(d)?))
1088}
1089
1090/// The hunks of a unified diff, as Claude Code's `structuredPatch` has them.
1091pub fn hunks_of(diff: &str) -> Value {
1092    let mut hunks: Vec<Value> = Vec::new();
1093    for line in diff.lines() {
1094        if let Some(head) = line.strip_prefix("@@ ") {
1095            let num = |sign: char| {
1096                head.split_whitespace()
1097                    .find_map(|w| w.strip_prefix(sign))
1098                    .and_then(|w| w.split(',').next())
1099                    .and_then(|n| n.parse::<u64>().ok())
1100                    .unwrap_or(0)
1101            };
1102            hunks.push(
1103                serde_json::json!({ "oldStart": num('-'), "newStart": num('+'), "lines": [] }),
1104            );
1105            continue;
1106        }
1107        if line.starts_with("+++") || line.starts_with("---") {
1108            continue;
1109        }
1110        if hunks.is_empty() {
1111            if !(line.starts_with('+') || line.starts_with('-') || line.starts_with(' ')) {
1112                continue;
1113            }
1114            hunks.push(serde_json::json!({ "oldStart": 1, "newStart": 1, "lines": [] }));
1115        }
1116        if let Some(lines) = hunks.last_mut().and_then(|h| h["lines"].as_array_mut()) {
1117            lines.push(serde_json::json!(line));
1118        }
1119    }
1120    Value::Array(hunks)
1121}
1122
1123/// A write a permission would make: its file, before (None for a file added) and after.
1124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1125#[serde(rename_all = "camelCase")]
1126pub struct Diff {
1127    pub path: String,
1128    #[serde(default)]
1129    pub old_text: Option<String>,
1130    #[serde(default)]
1131    pub new_text: String,
1132}
1133
1134/// What `engine/oneShot` is told: one question, no session, no settings of the user's, no MCP server.
1135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1136#[serde(rename_all = "camelCase")]
1137pub struct OneShotParams {
1138    /// `light` (a short text, no tool) or `reading` (after reading its folder, tools that only read).
1139    pub weight: String,
1140    pub cwd: String,
1141    pub system: String,
1142    pub prompt: String,
1143    #[serde(default)]
1144    pub model: String,
1145}
1146
1147/// How the core runs it: the arguments, the variables, and what goes on its input.
1148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1149#[serde(rename_all = "camelCase")]
1150pub struct OneShotLaunch {
1151    #[serde(default)]
1152    pub args: Vec<String>,
1153    #[serde(default)]
1154    pub env: Vec<EnvVar>,
1155    #[serde(default)]
1156    pub remove_env: Vec<String>,
1157    #[serde(default)]
1158    pub input: Option<String>,
1159}
1160
1161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1162pub struct OneShotOutput {
1163    pub stdout: String,
1164    pub stderr: String,
1165    #[serde(default)]
1166    pub code: Option<i32>,
1167}
1168
1169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1170pub struct OneShotText {
1171    pub text: String,
1172}
1173
1174/// What `terminal/launch` is told: the agent's interface in a terminal, its hooks pointed at Escouade.
1175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1176#[serde(rename_all = "camelCase")]
1177pub struct TerminalParams {
1178    pub agent: String,
1179    pub cwd: String,
1180    #[serde(default)]
1181    pub session: Option<String>,
1182    #[serde(default)]
1183    pub model: String,
1184    #[serde(default)]
1185    pub mode: String,
1186    #[serde(default)]
1187    pub home: Option<String>,
1188    /// Where its hooks post (`http://127.0.0.1:<port>/hook`), the variable that holds their token, and
1189    /// `escouade-hook` for a harness that runs its hooks as commands.
1190    pub hook_url: String,
1191    pub hook_token_var: String,
1192    #[serde(default)]
1193    pub hook_command: Option<String>,
1194    #[serde(default)]
1195    pub escouade: Option<Grant>,
1196    #[serde(default)]
1197    pub fence: Vec<String>,
1198    #[serde(default)]
1199    pub read_only: bool,
1200}
1201
1202/// One of its hooks, and what the core has to answer it: the hook goes both ways (what Escouade adds for the model, a call it refuses).
1203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1204pub struct Hook {
1205    pub agent: String,
1206    pub hook: Value,
1207    #[serde(default)]
1208    pub reply: Option<HookReply>,
1209}
1210
1211/// What the core answers a hook, which the plugin puts in its harness's form.
1212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1213pub struct HookReply {
1214    /// Context added for the model.
1215    #[serde(default)]
1216    pub context: Option<String>,
1217    /// The call refused, and why.
1218    #[serde(default)]
1219    pub deny: Option<String>,
1220}
1221
1222/// What a hook made: items for the agent, and the hook's answer as its harness reads it (None: nothing).
1223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1224pub struct HookAnswer {
1225    #[serde(default)]
1226    pub items: Vec<Item>,
1227    #[serde(default)]
1228    pub answer: Option<Value>,
1229}
1230
1231/// What the conversation holds of an id, for a view that starts on it.
1232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1233pub struct ViewKnown {
1234    pub id: String,
1235    /// A call's tool, and how it stands (`running`, `ok`…): none for what is no call.
1236    #[serde(default, skip_serializing_if = "Option::is_none")]
1237    pub name: Option<String>,
1238    #[serde(default, skip_serializing_if = "Option::is_none")]
1239    pub status: Option<String>,
1240}
1241
1242/// `view/translate`: the terminal view of an agent. The core reads what the harness's own interface writes (its transcript,
1243/// its rollout, its journal) and hands it on here a line at a time, what it holds of the agent beside it; the plugin answers
1244/// the items. The core keeps the terminal, the file and the agent's state; the plugin, what the lines mean.
1245///
1246/// A line is what the file holds, or a note of the core, an object with a field `escouade`:
1247/// - `{"escouade":"end","error":…,"durationMs":…,"models":…}`: the running turn is over, for a reason the core knows (its
1248///   interface gone, a hook, what `Item::ViewEnd` asked): the plugin answers its `Item::Turn`;
1249/// - `{"escouade":"hook","hook":…}`: a hook of the interface that says what the file does not;
1250/// - `{"escouade":"frame","frame":…}`: a message of the harness, read as a process's (a recorded stream).
1251#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1252#[serde(rename_all = "camelCase")]
1253pub struct ViewParams {
1254    /// The view, by a key of its own: another one is another conversation, which starts from nothing.
1255    pub agent: String,
1256    /// The harness, for a plugin that has several.
1257    #[serde(default)]
1258    pub harness: String,
1259    /// The first lines of this view: `known` is given, and `gone`, the view it replaces, may be dropped.
1260    #[serde(default)]
1261    pub fresh: bool,
1262    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1263    pub known: Vec<ViewKnown>,
1264    #[serde(default, skip_serializing_if = "Option::is_none")]
1265    pub gone: Option<String>,
1266    pub cwd: String,
1267    #[serde(default)]
1268    pub mode: String,
1269    #[serde(default)]
1270    pub model: String,
1271    /// The language of the app.
1272    #[serde(default)]
1273    pub lang: String,
1274    /// The model the agent last ran on.
1275    #[serde(default)]
1276    pub main_model: String,
1277    #[serde(default)]
1278    pub session: Option<String>,
1279    /// A turn runs.
1280    #[serde(default)]
1281    pub active: bool,
1282    /// The user stopped it.
1283    #[serde(default)]
1284    pub interrupted: bool,
1285    /// The agent is a copy, its session not its own yet.
1286    #[serde(default)]
1287    pub copy: bool,
1288    /// A discussion of the Docs tab: it writes nothing.
1289    #[serde(default)]
1290    pub discussion: bool,
1291    /// The message of the turn under way, as the interface took it.
1292    #[serde(default)]
1293    pub prompt: Option<String>,
1294    pub lines: Vec<String>,
1295}
1296
1297/// A harness as a manifest declares it (`contributes.harnesses[]`): all the core needs without starting the plugin
1298/// (the welcome screen, the menus, the accounts, the memory).
1299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1300#[serde(rename_all = "camelCase")]
1301pub struct HarnessDecl {
1302    pub id: String,
1303    pub name: String,
1304    #[serde(default)]
1305    pub speaker: Option<String>,
1306    /// Its program: a path (`{escouade}/…` for one of the app's), or names looked for on the PATH.
1307    #[serde(default)]
1308    pub locate: Locate,
1309    #[serde(default = "version_args")]
1310    pub version_args: Vec<String>,
1311    #[serde(default)]
1312    pub tested: TestedDecl,
1313    #[serde(default)]
1314    pub caps: CapsDecl,
1315    #[serde(default)]
1316    pub models: Vec<ModelDecl>,
1317    #[serde(default)]
1318    pub efforts: Vec<String>,
1319    /// The variable that gives each account its configuration folder.
1320    #[serde(default)]
1321    pub account_var: Option<String>,
1322    /// The variable that gives an account added by its key that key (`CURSOR_API_KEY`): the agent then goes by it, not
1323    /// by the machine's sign-in (`LaunchParams::by_key`).
1324    #[serde(default)]
1325    pub key_var: Option<String>,
1326    /// It does not end when its input is closed (Cursor CLI's `agent acp`): it is killed that many milliseconds after.
1327    #[serde(default)]
1328    pub close_grace_ms: Option<u64>,
1329    /// What frames its messages on its input and output: lines when left out, `contentLength` for `Content-Length`
1330    /// headers (Copilot CLI's server).
1331    #[serde(default)]
1332    pub framing: Option<String>,
1333    /// A line written on its input just before it is closed (Copilot CLI's `runtime.shutdown`, which saves its session).
1334    #[serde(default)]
1335    pub close_line: Option<String>,
1336    /// Its plugin opens the session over the protocol once it runs: the agent is ready at `Item::Ready`, not at its
1337    /// launch, and `Item::Stop` is its refusal.
1338    #[serde(default)]
1339    pub opens: bool,
1340    /// One agent has several programs in a row (`Item::Relaunch`, `Item::Pause`): it takes its settings at its launch alone,
1341    /// or has no way to stop a turn. Its end by itself is told to its plugin (`method::PROGRAM_GONE`).
1342    #[serde(default)]
1343    pub relaunches: bool,
1344    /// The lines of its error output that hold one of these are not kept (what it logs of the account).
1345    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1346    pub stderr_leaves_out: Vec<String>,
1347    /// Where it keeps what it remembers of a project, offered once to the shared memory: a folder of files, with
1348    /// `{configDir}` and `{projectKey}` (the project's path, every character but a letter or a digit a dash).
1349    #[serde(default)]
1350    pub memory: Option<MemoryDecl>,
1351    /// A harness that speaks ACP, described: how the plugin `dev.escouade.acp` drives it (PLG-6). A plugin that only
1352    /// describes such a harness runs no program of its own.
1353    #[serde(default)]
1354    pub acp: Option<AcpDecl>,
1355    /// How to install it, shown when it is not found.
1356    #[serde(default)]
1357    pub install: Option<InstallDecl>,
1358    /// The command that updates it.
1359    #[serde(default)]
1360    pub update_command: Option<String>,
1361    /// Who writes the texts it drafts (« rédigé par … »), its name when left out.
1362    #[serde(default)]
1363    pub writer: Option<String>,
1364}
1365
1366/// A text in the languages of the app: one for all, or by language (`{ "fr": "…", "en": "…" }`, English when the
1367/// language is missing).
1368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1369#[serde(untagged)]
1370pub enum Text {
1371    One(String),
1372    ByLang(std::collections::BTreeMap<String, String>),
1373}
1374
1375impl Text {
1376    /// In `lang`, else English, else any.
1377    pub fn get(&self, lang: &str) -> String {
1378        match self {
1379            Text::One(t) => t.clone(),
1380            Text::ByLang(m) => m
1381                .get(lang)
1382                .or_else(|| m.get("en"))
1383                .or_else(|| m.values().next())
1384                .cloned()
1385                .unwrap_or_default(),
1386        }
1387    }
1388}
1389
1390/// A mode of Escouade a harness takes (`default`, `plan`, `acceptEdits`, `bypassPermissions`), and what it means
1391/// there, as the menu says it. Written as its id alone, or `{ "id", "meaning" }`.
1392#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1393pub struct ModeDecl {
1394    pub id: String,
1395    #[serde(default, skip_serializing_if = "Option::is_none")]
1396    pub meaning: Option<Text>,
1397}
1398
1399impl<'de> Deserialize<'de> for ModeDecl {
1400    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1401        #[derive(Deserialize)]
1402        #[serde(untagged)]
1403        enum Form {
1404            Id(String),
1405            Full {
1406                id: String,
1407                #[serde(default)]
1408                meaning: Option<Text>,
1409            },
1410        }
1411        Ok(match Form::deserialize(d)? {
1412            Form::Id(id) => ModeDecl { id, meaning: None },
1413            Form::Full { id, meaning } => ModeDecl { id, meaning },
1414        })
1415    }
1416}
1417
1418impl From<&str> for ModeDecl {
1419    fn from(id: &str) -> Self {
1420        ModeDecl {
1421            id: id.into(),
1422            meaning: None,
1423        }
1424    }
1425}
1426
1427/// Its installers.
1428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1429pub struct InstallDecl {
1430    #[serde(default)]
1431    pub windows: String,
1432    #[serde(default)]
1433    pub unix: String,
1434}
1435
1436/// How an ACP agent is driven (ACP v1: JSON-RPC 2.0, one message a line on its input and output).
1437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1438#[serde(rename_all = "camelCase")]
1439pub struct AcpDecl {
1440    /// Its arguments (`["acp"]`).
1441    #[serde(default)]
1442    pub args: Vec<String>,
1443    /// Variables of its own (never a secret: a manifest is read by all).
1444    #[serde(default)]
1445    pub env: std::collections::BTreeMap<String, String>,
1446    /// Variables of the app's environment it must not get.
1447    #[serde(default)]
1448    pub remove_env: Vec<String>,
1449    /// It reads and writes files through the client (`fs/read_text_file`, `fs/write_text_file`): the fence of
1450    /// private projects is then held there. False: it reads and writes by itself, and no private project is open to it.
1451    #[serde(default = "yes")]
1452    pub fs: bool,
1453    /// With `fs` false, the fence of a private project held on its permissions alone, as Cursor CLI's in the core was
1454    /// (ESC-140): a permission that names a path of the fence is refused at once; what it reads and writes by itself
1455    /// is not seen, which its modes' meanings say. Only a plugin Escouade gives is believed (`fenceByClient`).
1456    #[serde(default)]
1457    pub fence_on_permissions: bool,
1458    /// The ACP mode (`session/set_mode`) of each mode of Escouade; a mode left out sets none.
1459    #[serde(default)]
1460    pub modes: std::collections::BTreeMap<String, String>,
1461    /// Its ACP mode that changes nothing (`ask`): with it, it holds a read-only launch (a discussion of the Docs tab).
1462    #[serde(default)]
1463    pub read_only_mode: Option<String>,
1464    /// The id of the configuration option that picks the model (`session/set_config_option`); left out, the model
1465    /// goes by `session/set_model`.
1466    #[serde(default)]
1467    pub model_option: Option<String>,
1468    /// The extensions of the protocol it speaks beside ACP (`cursor`: `cursor/ask_question`, `cursor/create_plan`,
1469    /// `cursor/update_todos`, its subagents).
1470    #[serde(default)]
1471    pub extensions: Vec<String>,
1472    /// It has no tool to ask the user: its first message tells it to end its turn on a block
1473    /// `<escouade_question>`, shown as a question's card, the answers sent as the next message (as for Gemini CLI).
1474    #[serde(default)]
1475    pub questions_in_text: bool,
1476    /// The names of its tools that run a subagent (by their title or `rawInput.tool`): shown as Claude Code's `Task`,
1477    /// a row of the plan.
1478    #[serde(default)]
1479    pub subagent_tools: Vec<String>,
1480    /// Its tools of the kind `other` under the name Claude Code gives them (`{ "shell": "Bash", "write": "Write" }`),
1481    /// by their own name: `_meta.<agent>.toolCall.toolName`, else their title before « · ».
1482    #[serde(default)]
1483    pub tools: std::collections::BTreeMap<String, String>,
1484}
1485
1486fn yes() -> bool {
1487    true
1488}
1489
1490fn version_args() -> Vec<String> {
1491    vec!["--version".into()]
1492}
1493
1494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1495pub struct Locate {
1496    #[serde(default)]
1497    pub program: Option<String>,
1498    #[serde(default)]
1499    pub names: Vec<String>,
1500}
1501
1502#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1503pub struct TestedDecl {
1504    pub oldest: String,
1505    pub newest: String,
1506}
1507
1508impl Default for TestedDecl {
1509    fn default() -> Self {
1510        Self {
1511            oldest: "0".into(),
1512            newest: "0".into(),
1513        }
1514    }
1515}
1516
1517/// What it can do; what is left out, it cannot.
1518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1519#[serde(rename_all = "camelCase")]
1520pub struct CapsDecl {
1521    #[serde(default)]
1522    pub modes: Vec<ModeDecl>,
1523    #[serde(default)]
1524    pub effort: bool,
1525    #[serde(default)]
1526    pub images: bool,
1527    #[serde(default)]
1528    pub pdf: bool,
1529    #[serde(default)]
1530    pub slash_commands: bool,
1531    #[serde(default)]
1532    pub terminal_view: bool,
1533    /// It holds a read-only launch (a discussion of the Docs tab): a security guarantee the core checks on the
1534    /// description of each launch.
1535    #[serde(default)]
1536    pub read_only: bool,
1537    /// Its accounts go by an API key of their own.
1538    #[serde(default)]
1539    pub accounts_by_key: bool,
1540}
1541
1542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1543pub struct ModelDecl {
1544    pub id: String,
1545    #[serde(default)]
1546    pub label: Option<String>,
1547    #[serde(default)]
1548    pub effort: bool,
1549}
1550
1551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1552pub struct MemoryDecl {
1553    pub dir: String,
1554}
1555
1556#[cfg(test)]
1557mod tests {
1558    use super::*;
1559    use serde_json::json;
1560
1561    #[test]
1562    fn an_item_of_an_unknown_op_is_left_aside_and_the_known_ones_read() {
1563        let items: Items = serde_json::from_value(json!({ "items": [
1564            { "op": "text", "text": "Bonjour" },
1565            { "op": "later", "what": 1 },
1566            { "op": "turn", "usage": { "input": 3 } },
1567        ] }))
1568        .unwrap();
1569        assert_eq!(
1570            items.items[0],
1571            Item::Text {
1572                text: "Bonjour".into(),
1573                parent: None,
1574                id: None,
1575            }
1576        );
1577        assert_eq!(items.items[1], Item::Unknown);
1578        assert!(matches!(items.items[2], Item::Turn { usage, .. } if usage.input == 3));
1579    }
1580
1581    #[test]
1582    fn a_grant_and_a_variable_never_print_their_secret() {
1583        let g = Grant {
1584            url: "http://127.0.0.1:1/mcp".into(),
1585            token: "secret".into(),
1586        };
1587        let v = EnvVar {
1588            name: "TOKEN".into(),
1589            value: "secret".into(),
1590        };
1591        assert!(!format!("{g:?}{v:?}").contains("secret"));
1592    }
1593
1594    #[test]
1595    fn a_declaration_takes_its_defaults() {
1596        let d: HarnessDecl = serde_json::from_value(json!({ "id": "x", "name": "X" })).unwrap();
1597        assert_eq!(d.version_args, ["--version"]);
1598        assert_eq!(d.tested.oldest, "0");
1599        assert!(!d.caps.read_only && d.models.is_empty());
1600        assert!(d.acp.is_none() && d.install.is_none());
1601    }
1602
1603    #[test]
1604    fn an_acp_harness_reads_with_its_modes_by_id_or_with_their_meaning() {
1605        let d: HarnessDecl = serde_json::from_value(json!({
1606            "id": "goose", "name": "Goose",
1607            "caps": { "modes": ["default", { "id": "bypassPermissions", "meaning": { "fr": "Tout", "en": "All" } }] },
1608            "acp": { "args": ["acp"], "modes": { "bypassPermissions": "auto" } },
1609            "install": { "unix": "curl …" },
1610        }))
1611        .unwrap();
1612        assert_eq!(d.caps.modes[0], ModeDecl::from("default"));
1613        assert_eq!(d.caps.modes[1].meaning.as_ref().unwrap().get("fr"), "Tout");
1614        assert_eq!(d.caps.modes[1].meaning.as_ref().unwrap().get("de"), "All");
1615        let acp = d.acp.unwrap();
1616        assert!(acp.fs, "the files go through the client unless it says not");
1617        assert_eq!(acp.args, ["acp"]);
1618        assert_eq!(acp.modes["bypassPermissions"], "auto");
1619        // Items of the contract it adds read too, and the old ones still.
1620        let items: Items = serde_json::from_value(json!({ "items": [
1621            { "op": "toolInput", "id": "c", "input": { "file_path": "a" } },
1622            { "op": "permission", "request": "r", "call": "c", "canAlways": true, "diff": { "path": "a", "newText": "x" } },
1623            { "op": "permission", "request": "r2", "call": "c2" },
1624            { "op": "todos", "todos": [] },
1625            { "op": "turn", "error": "refusé" },
1626        ] }))
1627        .unwrap();
1628        assert!(
1629            matches!(&items.items[1], Item::Permission { can_always: true, diff: Some(d), .. } if d.old_text.is_none())
1630        );
1631        assert!(matches!(&items.items[4], Item::Turn { error: Some(e), .. } if e == "refusé"));
1632        // A limit without its reset goes and comes back as one, none as none.
1633        for limited in [None, Some(None), Some(Some(5))] {
1634            let turn = Item::Turn {
1635                interrupted: false,
1636                limited,
1637                usage: TurnUsage::default(),
1638                error: None,
1639                entry: None,
1640            };
1641            let back: Item = serde_json::from_value(serde_json::to_value(&turn).unwrap()).unwrap();
1642            assert_eq!(back, turn);
1643        }
1644    }
1645}