use serde::{Deserialize, Serialize};
use crate::dialect::Dialect;
use crate::env::{EnvPolicy, EnvSource};
use crate::model::{
AgentSpec, BindingReceiptShape, CliLaneSpec, CommandSpec, LaneSpec, PromptChannel, RpcCallSpec,
RpcCompletionSpec, RpcEnvelopeSpec, RpcLaneSpec, RpcTransport, SessionScope, StreamMode,
};
pub struct AgentCard {
pub id: &'static str,
pub label: &'static str,
pub dialect: &'static Dialect,
pub surface: AgentSurface,
pub alternates: &'static [AlternateLane],
pub detect: DetectSurface,
pub features: FeatureSurface,
pub usage: UsageSurface,
pub history: &'static [HistoryRoot],
}
impl AgentCard {
pub fn spec(&self) -> AgentSpec {
self.spec_for(&self.surface, self.detect.binaries.first().copied())
}
pub fn specs(&self) -> Vec<AgentSpec> {
std::iter::once(self.spec())
.chain(self.alternates.iter().map(|alternate| {
self.spec_for(
&alternate.surface,
alternate
.binary
.or_else(|| self.detect.binaries.first().copied()),
)
}))
.collect()
}
fn spec_for(&self, surface: &AgentSurface, binary: Option<&'static str>) -> AgentSpec {
let lane = match surface {
AgentSurface::Acp {
prefer_load,
auth_key,
..
} => LaneSpec::Acp {
mcp_servers: Vec::new(),
prefer_load: *prefer_load,
auth_key: auth_key.map(AcpAuthKey::source),
},
AgentSurface::Cli(cli) => LaneSpec::Cli(Box::new(CliLaneSpec {
args: cli.args.iter().map(|arg| (*arg).to_owned()).collect(),
prompt_channel: cli.prompt_channel,
stream: cli.stream,
resume_args: cli
.resume_args
.iter()
.map(|arg| (*arg).to_owned())
.collect(),
resume_spawn_args: cli
.resume_spawn_args
.iter()
.map(|arg| (*arg).to_owned())
.collect(),
stdin_template: cli.stdin_template.map(|text| {
serde_json::from_str(text).expect("a4aa card stdin template is json")
}),
json_lines: cli.json_lines,
session_id_path: cli.session_id_path.map(str::to_owned),
usage_path: cli
.usage_path
.map(str::to_owned)
.or_else(|| self.usage.live.as_ref().map(|usage| usage.path.to_owned())),
open_args: cli.open_args.iter().map(|arg| (*arg).to_owned()).collect(),
binding_file_env: cli.binding_file_env.map(str::to_owned),
binding_receipt_shape: cli.binding_receipt_shape,
result_selector: cli.result_selector.map(str::to_owned),
result_selector_field: cli.result_selector_field.map(str::to_owned),
result_path: cli.result_path.map(str::to_owned),
execution_modes: self
.features
.execution_modes
.iter()
.map(|mode| (*mode).to_owned())
.collect(),
permission_modes: self
.features
.permission_modes
.iter()
.map(|mode| (*mode).to_owned())
.collect(),
})),
AgentSurface::Rpc(rpc) => LaneSpec::Rpc(Box::new(RpcLaneSpec {
initialize: rpc.initialize.map(RpcCallDecl::call),
initialized: rpc.initialized.map(str::to_owned),
session_open: rpc.session_open.map(RpcCallDecl::call),
session_resume: rpc.session_resume.map(RpcCallDecl::call),
prompt: rpc.prompt.call(),
completion: rpc.completion.map(|done| RpcCompletionSpec {
method: done.method.to_owned(),
turn_id_path: done.turn_id_path.map(str::to_owned),
status_path: done.status_path.map(str::to_owned),
status_value: done.status_value.map(str::to_owned),
}),
cancel: rpc.cancel.map(RpcCallDecl::call),
session_close: rpc.session_close.map(RpcCallDecl::call),
text_event_method: rpc.text_event_method.map(str::to_owned),
text_path: rpc.text_path.map(str::to_owned),
result_text_path: rpc.result_text_path.map(str::to_owned),
pre_turn: rpc.pre_turn.iter().map(|decl| decl.call()).collect(),
session_scope: rpc.session_scope,
kill_on_cancel: rpc.kill_on_cancel,
envelope: rpc.envelope.map(|envelope| RpcEnvelopeSpec {
method_field: envelope.method_field.to_owned(),
string_ids: envelope.string_ids,
params_inline: envelope.params_inline,
response_method: envelope.response_method.map(str::to_owned),
data_field: envelope.data_field.map(str::to_owned),
result_field: envelope.result_field.map(str::to_owned),
error_field: envelope.error_field.map(str::to_owned),
success_field: envelope.success_field.map(str::to_owned),
command_field: envelope.command_field.map(str::to_owned),
discriminant_field: envelope.discriminant_field.map(str::to_owned),
request_marker: envelope.request_marker.map(str::to_owned),
notification_marker: envelope.notification_marker.map(str::to_owned),
const_fields: envelope
.const_fields
.iter()
.map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
.collect(),
}),
passthrough: rpc
.passthrough
.iter()
.map(|method| (*method).to_owned())
.collect(),
transport: match rpc.transport {
RpcTransportDecl::Stdio => RpcTransport::Stdio,
RpcTransportDecl::WebSocket(announce) => RpcTransport::WebSocket {
announce: announce.to_owned(),
},
},
})),
AgentSurface::Serve { .. } => LaneSpec::Serve {
mcp_servers: Vec::new(),
},
};
AgentSpec {
id: self.id.to_owned(),
client_name: None,
lane,
command: CommandSpec {
program: binary.unwrap_or(self.id).to_owned(),
args: Self::surface_spawn_args(surface)
.iter()
.map(|arg| (*arg).to_owned())
.collect(),
cwd: None,
env: EnvPolicy::default(),
},
max_output_bytes: None,
}
}
fn surface_spawn_args(surface: &AgentSurface) -> &'static [&'static str] {
match surface {
AgentSurface::Acp { spawn_args, .. }
| AgentSurface::Cli(CliSurface { spawn_args, .. })
| AgentSurface::Serve { spawn_args } => spawn_args,
AgentSurface::Rpc(rpc) => rpc.spawn_args,
}
}
}
pub enum AgentSurface {
Acp {
prefer_load: bool,
spawn_args: &'static [&'static str],
auth_key: Option<AcpAuthKey>,
},
Cli(CliSurface),
Rpc(&'static RpcSurface),
Serve { spawn_args: &'static [&'static str] },
}
pub struct AlternateLane {
pub surface: AgentSurface,
pub binary: Option<&'static str>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AcpAuthKey {
HostEnv(&'static str),
Secret(&'static str),
}
impl AcpAuthKey {
fn source(self) -> EnvSource {
match self {
Self::HostEnv(name) => EnvSource::HostEnv {
host_env: name.to_owned(),
},
Self::Secret(reference) => EnvSource::Secret {
secret: reference.to_owned(),
},
}
}
}
pub struct CliSurface {
pub args: &'static [&'static str],
pub prompt_channel: PromptChannel,
pub stream: StreamMode,
pub resume_args: &'static [&'static str],
pub resume_spawn_args: &'static [&'static str],
pub stdin_template: Option<&'static str>,
pub json_lines: bool,
pub session_id_path: Option<&'static str>,
pub open_args: &'static [&'static str],
pub binding_file_env: Option<&'static str>,
pub binding_receipt_shape: BindingReceiptShape,
pub result_selector: Option<&'static str>,
pub result_selector_field: Option<&'static str>,
pub result_path: Option<&'static str>,
pub usage_path: Option<&'static str>,
pub spawn_args: &'static [&'static str],
}
pub struct RpcSurface {
pub initialize: Option<RpcCallDecl>,
pub initialized: Option<&'static str>,
pub session_open: Option<RpcCallDecl>,
pub session_resume: Option<RpcCallDecl>,
pub prompt: RpcCallDecl,
pub cancel: Option<RpcCallDecl>,
pub session_close: Option<RpcCallDecl>,
pub text_event_method: Option<&'static str>,
pub text_path: Option<&'static str>,
pub completion: Option<CompletionDecl>,
pub result_text_path: Option<&'static str>,
pub pre_turn: &'static [RpcCallDecl],
pub session_scope: SessionScope,
pub kill_on_cancel: bool,
pub envelope: Option<&'static EnvelopeDecl>,
pub passthrough: &'static [&'static str],
pub spawn_args: &'static [&'static str],
pub transport: RpcTransportDecl,
}
#[derive(Clone, Copy)]
pub enum RpcTransportDecl {
Stdio,
WebSocket(&'static str),
}
#[derive(Clone, Copy)]
pub struct EnvelopeDecl {
pub method_field: &'static str,
pub string_ids: bool,
pub params_inline: bool,
pub response_method: Option<&'static str>,
pub data_field: Option<&'static str>,
pub result_field: Option<&'static str>,
pub error_field: Option<&'static str>,
pub success_field: Option<&'static str>,
pub command_field: Option<&'static str>,
pub discriminant_field: Option<&'static str>,
pub request_marker: Option<&'static str>,
pub notification_marker: Option<&'static str>,
pub const_fields: &'static [(&'static str, &'static str)],
}
#[derive(Clone, Copy)]
pub struct RpcCallDecl {
pub method: &'static str,
pub params: &'static str,
}
impl RpcCallDecl {
fn call(self) -> RpcCallSpec {
RpcCallSpec {
method: self.method.to_owned(),
params: serde_json::from_str(self.params).expect("a4aa card rpc params are json"),
}
}
}
#[derive(Clone, Copy)]
pub struct CompletionDecl {
pub method: &'static str,
pub turn_id_path: Option<&'static str>,
pub status_path: Option<&'static str>,
pub status_value: Option<&'static str>,
}
pub struct DetectSurface {
pub binaries: &'static [&'static str],
pub processes: &'static [&'static str],
pub binary_dirs: &'static [OsPath],
pub config: &'static [&'static str],
pub detection: &'static [&'static str],
pub apps: &'static [OsPath],
}
pub struct OsPath {
pub path: &'static str,
pub oses: &'static [&'static str],
}
pub struct HistoryRoot {
pub path: &'static str,
pub kind: &'static str,
pub store: StoreKind,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StoreKind {
JsonlFile,
JsonlDir,
SqliteDb,
MarkdownFile,
Directory,
Other,
}
pub struct UsageSurface {
pub live: Option<LiveUsage>,
pub transcript: Option<TranscriptUsage>,
pub hosted: Option<&'static str>,
}
pub struct LiveUsage {
pub event: &'static str,
pub path: &'static str,
}
pub struct TranscriptUsage {
pub root: &'static str,
pub record: Option<(&'static str, &'static str)>,
pub container: &'static str,
pub input: &'static [&'static str],
pub cached: &'static [&'static str],
pub output: &'static [&'static str],
pub total: &'static [&'static str],
}
pub struct FeatureSurface {
pub instructions: InstructionChannel,
pub execution_modes: &'static [&'static str],
pub permission_modes: &'static [&'static str],
pub permission_default: Option<&'static str>,
pub attachments: AttachmentSupport,
pub effort: bool,
pub steer: bool,
pub models: ModelCatalog,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InstructionChannel {
Compose,
Param(&'static str),
Flag(&'static str),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AttachmentSupport {
None,
LocalImages,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ModelCatalog {
None,
RpcMethod(&'static str),
CliArgs(&'static [&'static str]),
}