use serde::{Deserialize, Serialize};
use serde_json::Value;
pub mod method {
pub const LAUNCH: &str = "engine/launch";
pub const ONE_SHOT: &str = "engine/oneShot";
pub const ONE_SHOT_READ: &str = "engine/oneShotRead";
pub const ATTACH: &str = "agent/attach";
pub const DETACH: &str = "agent/detach";
pub const FRAMES: &str = "agent/frames";
pub const SEND_MESSAGE: &str = "agent/sendMessage";
pub const INTERRUPT: &str = "agent/interrupt";
pub const SET_MODEL: &str = "agent/setModel";
pub const SET_EFFORT: &str = "agent/setEffort";
pub const SET_MODE: &str = "agent/setMode";
pub const ANSWER_PERMISSION: &str = "agent/answerPermission";
pub const ANSWER_QUESTION: &str = "agent/answerQuestion";
pub const READ_USAGE: &str = "agent/readUsage";
pub const TERMINAL_LAUNCH: &str = "terminal/launch";
pub const TERMINAL_HOOK: &str = "terminal/hook";
pub const OPEN: &str = "agent/open";
pub const CLOSE: &str = "agent/close";
pub const LET_GO: &str = "agent/letGo";
pub const COMPACT: &str = "agent/compact";
pub const RENAME: &str = "agent/rename";
pub const CONTROL: &str = "agent/control";
pub const ITEMS: &str = "agent/items";
pub const PROGRAM_GONE: &str = "agent/programGone";
pub const STOP_TASK: &str = "agent/stopTask";
pub const TASK_OUTPUT: &str = "agent/taskOutput";
pub const REMOTE: &str = "agent/remote";
}
pub const BATCH_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum StartAt {
#[default]
New,
Resume {
session: String,
},
Fork {
of: String,
#[serde(default)]
at: Option<String>,
},
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Grant {
pub url: String,
pub token: String,
}
impl std::fmt::Debug for Grant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Grant")
.field("url", &self.url)
.field("token", &"…")
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct LaunchParams {
pub agent: String,
pub cwd: String,
pub model: String,
#[serde(default)]
pub effort: String,
pub mode: String,
#[serde(default)]
pub append_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relay: Option<String>,
#[serde(default)]
pub by_key: bool,
#[serde(default)]
pub start: StartAt,
#[serde(default)]
pub read_dirs: Vec<String>,
#[serde(default)]
pub escouade: Option<Grant>,
#[serde(default)]
pub fence: Vec<String>,
#[serde(default)]
pub read_only: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub escouade_file: Option<String>,
#[serde(default)]
pub program: String,
#[serde(default)]
pub home: Option<String>,
#[serde(default)]
pub described: Option<HarnessDecl>,
#[serde(default)]
pub lang: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub takes_effort: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub settings: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refusal_file: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvVar {
pub name: String,
pub value: String,
}
impl std::fmt::Debug for EnvVar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}=…", self.name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileToWrite {
pub path: String,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Launch {
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: Vec<EnvVar>,
#[serde(default)]
pub remove_env: Vec<String>,
#[serde(default)]
pub files: Vec<FileToWrite>,
#[serde(default)]
pub fence_applied: bool,
#[serde(default)]
pub read_only: bool,
#[serde(default)]
pub fence_by_client: bool,
#[serde(default)]
pub writes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shared: Option<SharedLaunch>,
#[serde(default)]
pub trust: Vec<String>,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SharedLaunch {
pub ready: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub listens: Option<String>,
#[serde(default)]
pub one_agent_a_folder: bool,
#[serde(default)]
pub keep: Value,
}
impl std::fmt::Debug for SharedLaunch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedLaunch")
.field("ready", &self.ready)
.field("listens", &self.listens)
.field("one_agent_a_folder", &self.one_agent_a_folder)
.field("keep", &"…")
.finish()
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct SharedRef {
pub key: String,
pub line: String,
#[serde(default)]
pub keep: Value,
}
impl std::fmt::Debug for SharedRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedRef")
.field("key", &self.key)
.field("line", &self.line)
.field("keep", &"…")
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Attach {
pub agent: String,
pub cwd: String,
#[serde(default)]
pub session: Option<String>,
#[serde(default)]
pub model: String,
#[serde(default)]
pub effort: String,
#[serde(default)]
pub mode: String,
#[serde(default)]
pub pending: Vec<String>,
#[serde(default)]
pub described: Option<HarnessDecl>,
#[serde(default)]
pub lang: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shared: Option<SharedRef>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub roots: std::collections::BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fence: Vec<String>,
#[serde(default)]
pub read_only: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub append_prompt: Option<String>,
#[serde(default)]
pub running: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub home: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub read_dirs: Vec<String>,
#[serde(default)]
pub by_key: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Open {
pub agent: String,
#[serde(default)]
pub start: StartAt,
#[serde(default)]
pub escouade: Option<Grant>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Opened {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attach: Option<Launch>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Close {
pub agent: String,
#[serde(default)]
pub now: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Compact {
pub agent: String,
#[serde(default)]
pub instructions: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Compacted {
#[serde(default)]
pub over: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub writes: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub turn: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Control {
pub agent: String,
pub request: Value,
#[serde(default)]
pub within_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Pushed {
pub agent: String,
pub items: Vec<Item>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentRef {
pub agent: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Interrupt {
pub agent: String,
#[serde(default)]
pub cancel_queued: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Gone {
pub agent: String,
#[serde(default)]
pub code: Option<i32>,
#[serde(default)]
pub said: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Frames {
pub agent: String,
pub frames: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Items {
#[serde(default)]
pub items: Vec<Item>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Writes {
#[serde(default)]
pub writes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SendMessage {
pub agent: String,
pub uuid: String,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetValue {
pub agent: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnswerPermission {
pub agent: String,
pub request: String,
pub allow: bool,
#[serde(default)]
pub always: bool,
#[serde(default)]
pub input: Value,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub resume: Option<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub call: String,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub rules: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AnswerQuestion {
pub agent: String,
pub request: String,
pub call: String,
pub answers: Value,
#[serde(default)]
pub questions: Value,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub input: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Usage {
#[serde(default)]
pub usage: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct UsageAsked {
#[serde(default)]
pub writes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Asked {
#[serde(default)]
pub writes: Vec<String>,
#[serde(default)]
pub ask: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Task {
pub agent: String,
pub task: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoteLink {
pub agent: String,
pub enabled: bool,
#[serde(default)]
pub name: String,
#[serde(default)]
pub keep: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reattach: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ModelTurn {
pub model: String,
#[serde(default)]
pub input: u64,
#[serde(default)]
pub output: u64,
#[serde(default)]
pub cache: u64,
#[serde(default)]
pub cost: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
pub struct TurnUsage {
#[serde(default)]
pub input: u64,
#[serde(default)]
pub output: u64,
#[serde(default)]
pub cache: u64,
#[serde(default)]
pub cost: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "camelCase")]
pub enum Item {
Start {
#[serde(default)]
session: Option<String>,
},
Session { session: String },
#[serde(rename_all = "camelCase")]
Text {
text: String,
#[serde(default)]
parent: Option<String>,
#[serde(default)]
id: Option<String>,
},
Thinking {
text: String,
#[serde(default)]
parent: Option<String>,
#[serde(default)]
id: Option<String>,
},
Delta {
id: String,
text: String,
#[serde(default)]
thinking: bool,
#[serde(default)]
parent: Option<String>,
},
Subagent { call: String, model: String },
Tool {
id: String,
name: String,
#[serde(default)]
input: Value,
#[serde(default)]
parent: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
unplanned: bool,
},
ToolInput {
id: String,
#[serde(default)]
name: Option<String>,
#[serde(default)]
input: Value,
},
ToolDone {
id: String,
#[serde(default)]
output: String,
#[serde(default)]
error: bool,
#[serde(default)]
result: Value,
},
Touched { path: String },
#[serde(rename_all = "camelCase")]
Permission {
request: String,
call: String,
#[serde(default)]
tool: Option<String>,
#[serde(default)]
input: Option<Value>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
reason: Option<String>,
#[serde(default)]
diff: Option<Diff>,
#[serde(default)]
can_always: bool,
#[serde(default)]
parent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hunks: Option<Value>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
always_by_harness: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
ends_stream: bool,
},
Question {
request: String,
call: String,
questions: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
parent: Option<String>,
},
Answered {
request: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
decision: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
answers: Option<Value>,
},
User { id: String, text: String },
Model { model: String },
Context {
#[serde(default, skip_serializing_if = "Option::is_none")]
tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
window: Option<u64>,
},
Todos { todos: Value },
Mode { mode: String },
Turn {
#[serde(default)]
interrupted: bool,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
limited: Option<Option<i64>>,
#[serde(default)]
usage: TurnUsage,
#[serde(default)]
error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
entry: Option<String>,
},
Notice {
text: String,
#[serde(default = "info")]
level: String,
},
Write { line: String },
#[serde(rename_all = "camelCase")]
PermissionCard {
request: String,
call: String,
tool: String,
#[serde(default)]
input: Value,
#[serde(default)]
description: Option<String>,
#[serde(default)]
reason: Option<String>,
#[serde(default)]
hunks: Value,
#[serde(default)]
can_always: bool,
#[serde(default)]
always_scope: Option<String>,
#[serde(default)]
parent: Option<String>,
},
QuestionCard {
request: String,
call: String,
questions: Value,
#[serde(default)]
form: Value,
#[serde(default)]
parent: Option<String>,
},
Quota {
#[serde(default)]
meters: Value,
},
Usage {
#[serde(default)]
usage: Value,
},
LimitRest,
Ready {
#[serde(default)]
models: Value,
},
Compacted {
#[serde(default)]
error: Option<String>,
},
Stop { reason: String },
Entry {
#[serde(default)]
entry: Option<String>,
},
Activity {
#[serde(default)]
tool: Option<String>,
#[serde(default)]
input: Value,
#[serde(default)]
block: Option<String>,
},
Took { ms: i64 },
Relaunch { launch: Box<Launch> },
Pause,
File { path: String, text: String },
Closed,
Conversation {
session: String,
#[serde(default)]
notice: String,
},
Message {
id: String,
#[serde(default)]
model: String,
#[serde(default)]
usage: Value,
#[serde(default)]
main: bool,
},
MessageOutput { id: String, output: u64 },
Tasks { frame: Value },
Event {
#[serde(default)]
id: Option<String>,
source: String,
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
from: Option<String>,
},
Output {
#[serde(default)]
id: Option<String>,
level: String,
text: String,
},
Call {
id: String,
name: String,
#[serde(default)]
input: Value,
#[serde(default)]
parent: Option<String>,
},
ToolResult {
id: String,
#[serde(default)]
output: String,
#[serde(default)]
error: bool,
#[serde(default)]
result: Value,
},
Replayed {
#[serde(default)]
id: Option<String>,
#[serde(default)]
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
origin: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
from: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
synthetic: bool,
#[serde(default)]
images: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
files: Vec<String>,
},
#[serde(rename_all = "camelCase")]
Ask {
request: String,
call: String,
tool: String,
#[serde(default)]
input: Value,
#[serde(default)]
title: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
reason: Option<String>,
#[serde(default)]
suggestions: Value,
#[serde(default)]
no_always: bool,
#[serde(default)]
default_no: bool,
#[serde(default)]
task: Option<String>,
},
#[serde(rename_all = "camelCase")]
Result {
#[serde(default)]
id: Option<String>,
#[serde(default)]
failed: bool,
#[serde(default)]
error: Option<String>,
#[serde(default)]
failure: String,
#[serde(default)]
reply: String,
#[serde(default)]
limited: bool,
#[serde(default)]
resets: Option<i64>,
#[serde(default)]
sign_in: bool,
#[serde(default)]
duration: Value,
#[serde(default)]
window: Option<u64>,
#[serde(default)]
cost: f64,
#[serde(default)]
tokens: u64,
#[serde(default)]
models: Vec<ModelTurn>,
},
Limit {
#[serde(default)]
scoped: bool,
},
Overage { on: bool },
Remote {
#[serde(default)]
state: Option<String>,
},
Told {
#[serde(default)]
commands: Value,
#[serde(default)]
models: Value,
},
Answer {
ask: String,
#[serde(default)]
answer: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
Said { line: String },
#[serde(other)]
Unknown,
}
fn info() -> String {
"info".into()
}
fn present<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<Option<i64>>, D::Error> {
Ok(Some(Option::<i64>::deserialize(d)?))
}
pub fn hunks_of(diff: &str) -> Value {
let mut hunks: Vec<Value> = Vec::new();
for line in diff.lines() {
if let Some(head) = line.strip_prefix("@@ ") {
let num = |sign: char| {
head.split_whitespace()
.find_map(|w| w.strip_prefix(sign))
.and_then(|w| w.split(',').next())
.and_then(|n| n.parse::<u64>().ok())
.unwrap_or(0)
};
hunks.push(
serde_json::json!({ "oldStart": num('-'), "newStart": num('+'), "lines": [] }),
);
continue;
}
if line.starts_with("+++") || line.starts_with("---") {
continue;
}
if hunks.is_empty() {
if !(line.starts_with('+') || line.starts_with('-') || line.starts_with(' ')) {
continue;
}
hunks.push(serde_json::json!({ "oldStart": 1, "newStart": 1, "lines": [] }));
}
if let Some(lines) = hunks.last_mut().and_then(|h| h["lines"].as_array_mut()) {
lines.push(serde_json::json!(line));
}
}
Value::Array(hunks)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Diff {
pub path: String,
#[serde(default)]
pub old_text: Option<String>,
#[serde(default)]
pub new_text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OneShotParams {
pub weight: String,
pub cwd: String,
pub system: String,
pub prompt: String,
#[serde(default)]
pub model: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct OneShotLaunch {
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: Vec<EnvVar>,
#[serde(default)]
pub remove_env: Vec<String>,
#[serde(default)]
pub input: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OneShotOutput {
pub stdout: String,
pub stderr: String,
#[serde(default)]
pub code: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OneShotText {
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TerminalParams {
pub agent: String,
pub cwd: String,
#[serde(default)]
pub session: Option<String>,
#[serde(default)]
pub model: String,
#[serde(default)]
pub mode: String,
#[serde(default)]
pub home: Option<String>,
pub hook_url: String,
pub hook_token_var: String,
#[serde(default)]
pub hook_command: Option<String>,
#[serde(default)]
pub escouade: Option<Grant>,
#[serde(default)]
pub fence: Vec<String>,
#[serde(default)]
pub read_only: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Hook {
pub agent: String,
pub hook: Value,
#[serde(default)]
pub reply: Option<HookReply>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct HookReply {
#[serde(default)]
pub context: Option<String>,
#[serde(default)]
pub deny: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct HookAnswer {
#[serde(default)]
pub items: Vec<Item>,
#[serde(default)]
pub answer: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HarnessDecl {
pub id: String,
pub name: String,
#[serde(default)]
pub speaker: Option<String>,
#[serde(default)]
pub locate: Locate,
#[serde(default = "version_args")]
pub version_args: Vec<String>,
#[serde(default)]
pub tested: TestedDecl,
#[serde(default)]
pub caps: CapsDecl,
#[serde(default)]
pub models: Vec<ModelDecl>,
#[serde(default)]
pub efforts: Vec<String>,
#[serde(default)]
pub account_var: Option<String>,
#[serde(default)]
pub key_var: Option<String>,
#[serde(default)]
pub close_grace_ms: Option<u64>,
#[serde(default)]
pub framing: Option<String>,
#[serde(default)]
pub close_line: Option<String>,
#[serde(default)]
pub opens: bool,
#[serde(default)]
pub relaunches: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stderr_leaves_out: Vec<String>,
#[serde(default)]
pub memory: Option<MemoryDecl>,
#[serde(default)]
pub acp: Option<AcpDecl>,
#[serde(default)]
pub install: Option<InstallDecl>,
#[serde(default)]
pub update_command: Option<String>,
#[serde(default)]
pub writer: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Text {
One(String),
ByLang(std::collections::BTreeMap<String, String>),
}
impl Text {
pub fn get(&self, lang: &str) -> String {
match self {
Text::One(t) => t.clone(),
Text::ByLang(m) => m
.get(lang)
.or_else(|| m.get("en"))
.or_else(|| m.values().next())
.cloned()
.unwrap_or_default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ModeDecl {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub meaning: Option<Text>,
}
impl<'de> Deserialize<'de> for ModeDecl {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Form {
Id(String),
Full {
id: String,
#[serde(default)]
meaning: Option<Text>,
},
}
Ok(match Form::deserialize(d)? {
Form::Id(id) => ModeDecl { id, meaning: None },
Form::Full { id, meaning } => ModeDecl { id, meaning },
})
}
}
impl From<&str> for ModeDecl {
fn from(id: &str) -> Self {
ModeDecl {
id: id.into(),
meaning: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct InstallDecl {
#[serde(default)]
pub windows: String,
#[serde(default)]
pub unix: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct AcpDecl {
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: std::collections::BTreeMap<String, String>,
#[serde(default)]
pub remove_env: Vec<String>,
#[serde(default = "yes")]
pub fs: bool,
#[serde(default)]
pub fence_on_permissions: bool,
#[serde(default)]
pub modes: std::collections::BTreeMap<String, String>,
#[serde(default)]
pub read_only_mode: Option<String>,
#[serde(default)]
pub model_option: Option<String>,
#[serde(default)]
pub extensions: Vec<String>,
#[serde(default)]
pub questions_in_text: bool,
#[serde(default)]
pub subagent_tools: Vec<String>,
#[serde(default)]
pub tools: std::collections::BTreeMap<String, String>,
}
fn yes() -> bool {
true
}
fn version_args() -> Vec<String> {
vec!["--version".into()]
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Locate {
#[serde(default)]
pub program: Option<String>,
#[serde(default)]
pub names: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TestedDecl {
pub oldest: String,
pub newest: String,
}
impl Default for TestedDecl {
fn default() -> Self {
Self {
oldest: "0".into(),
newest: "0".into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CapsDecl {
#[serde(default)]
pub modes: Vec<ModeDecl>,
#[serde(default)]
pub effort: bool,
#[serde(default)]
pub images: bool,
#[serde(default)]
pub pdf: bool,
#[serde(default)]
pub slash_commands: bool,
#[serde(default)]
pub terminal_view: bool,
#[serde(default)]
pub read_only: bool,
#[serde(default)]
pub accounts_by_key: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelDecl {
pub id: String,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub effort: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemoryDecl {
pub dir: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn an_item_of_an_unknown_op_is_left_aside_and_the_known_ones_read() {
let items: Items = serde_json::from_value(json!({ "items": [
{ "op": "text", "text": "Bonjour" },
{ "op": "later", "what": 1 },
{ "op": "turn", "usage": { "input": 3 } },
] }))
.unwrap();
assert_eq!(
items.items[0],
Item::Text {
text: "Bonjour".into(),
parent: None,
id: None,
}
);
assert_eq!(items.items[1], Item::Unknown);
assert!(matches!(items.items[2], Item::Turn { usage, .. } if usage.input == 3));
}
#[test]
fn a_grant_and_a_variable_never_print_their_secret() {
let g = Grant {
url: "http://127.0.0.1:1/mcp".into(),
token: "secret".into(),
};
let v = EnvVar {
name: "TOKEN".into(),
value: "secret".into(),
};
assert!(!format!("{g:?}{v:?}").contains("secret"));
}
#[test]
fn a_declaration_takes_its_defaults() {
let d: HarnessDecl = serde_json::from_value(json!({ "id": "x", "name": "X" })).unwrap();
assert_eq!(d.version_args, ["--version"]);
assert_eq!(d.tested.oldest, "0");
assert!(!d.caps.read_only && d.models.is_empty());
assert!(d.acp.is_none() && d.install.is_none());
}
#[test]
fn an_acp_harness_reads_with_its_modes_by_id_or_with_their_meaning() {
let d: HarnessDecl = serde_json::from_value(json!({
"id": "goose", "name": "Goose",
"caps": { "modes": ["default", { "id": "bypassPermissions", "meaning": { "fr": "Tout", "en": "All" } }] },
"acp": { "args": ["acp"], "modes": { "bypassPermissions": "auto" } },
"install": { "unix": "curl …" },
}))
.unwrap();
assert_eq!(d.caps.modes[0], ModeDecl::from("default"));
assert_eq!(d.caps.modes[1].meaning.as_ref().unwrap().get("fr"), "Tout");
assert_eq!(d.caps.modes[1].meaning.as_ref().unwrap().get("de"), "All");
let acp = d.acp.unwrap();
assert!(acp.fs, "the files go through the client unless it says not");
assert_eq!(acp.args, ["acp"]);
assert_eq!(acp.modes["bypassPermissions"], "auto");
let items: Items = serde_json::from_value(json!({ "items": [
{ "op": "toolInput", "id": "c", "input": { "file_path": "a" } },
{ "op": "permission", "request": "r", "call": "c", "canAlways": true, "diff": { "path": "a", "newText": "x" } },
{ "op": "permission", "request": "r2", "call": "c2" },
{ "op": "todos", "todos": [] },
{ "op": "turn", "error": "refusé" },
] }))
.unwrap();
assert!(
matches!(&items.items[1], Item::Permission { can_always: true, diff: Some(d), .. } if d.old_text.is_none())
);
assert!(matches!(&items.items[4], Item::Turn { error: Some(e), .. } if e == "refusé"));
for limited in [None, Some(None), Some(Some(5))] {
let turn = Item::Turn {
interrupted: false,
limited,
usage: TurnUsage::default(),
error: None,
entry: None,
};
let back: Item = serde_json::from_value(serde_json::to_value(&turn).unwrap()).unwrap();
assert_eq!(back, turn);
}
}
}