use std::collections::HashSet;
use serde::{Deserialize, Serialize};
pub const DEFAULT_MODEL: &str = "gemini-3.5-flash";
pub const DEFAULT_IMAGE_GENERATION_MODEL: &str = "gemini-2.0-flash-exp-image-generation";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThinkingLevel {
Minimal,
Low,
Medium,
High,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemInstructionSection {
pub content: String,
#[serde(default = "default_section_title")]
pub title: String,
}
fn default_section_title() -> String {
"user_system_instructions".to_string()
}
impl Default for SystemInstructionSection {
fn default() -> Self {
Self {
content: String::new(),
title: default_section_title(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomSystemInstructions {
pub text: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TemplatedSystemInstructions {
#[serde(skip_serializing_if = "Option::is_none")]
pub identity: Option<String>,
#[serde(default)]
pub sections: Vec<SystemInstructionSection>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemInstructions {
Custom(CustomSystemInstructions),
Templated(TemplatedSystemInstructions),
}
impl From<&str> for SystemInstructions {
fn from(text: &str) -> Self {
Self::Custom(CustomSystemInstructions { text: text.into() })
}
}
impl From<String> for SystemInstructions {
fn from(text: String) -> Self {
Self::Custom(CustomSystemInstructions { text })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BuiltinTool {
ListDirectory,
SearchDirectory,
FindFile,
ViewFile,
CreateFile,
EditFile,
DeleteFile,
RenameFile,
RunCommand,
AskQuestion,
StartSubagent,
GenerateImage,
CallAgent,
CompileRustlite,
RunCartridge,
RenderHtml,
ConfigureAgent,
CurrentTime,
Finish,
}
impl BuiltinTool {
pub const ALL: &'static [BuiltinTool] = &[
Self::ListDirectory,
Self::SearchDirectory,
Self::FindFile,
Self::ViewFile,
Self::CreateFile,
Self::EditFile,
Self::DeleteFile,
Self::RenameFile,
Self::RunCommand,
Self::AskQuestion,
Self::StartSubagent,
Self::GenerateImage,
Self::CallAgent,
Self::CompileRustlite,
Self::RunCartridge,
Self::RenderHtml,
Self::ConfigureAgent,
Self::CurrentTime,
Self::Finish,
];
pub const READ_ONLY: &'static [BuiltinTool] = &[
Self::ListDirectory,
Self::SearchDirectory,
Self::FindFile,
Self::ViewFile,
Self::CurrentTime,
Self::Finish,
];
pub const FILE_TOOLS: &'static [BuiltinTool] = &[
Self::ViewFile,
Self::CreateFile,
Self::EditFile,
Self::DeleteFile,
Self::RenameFile,
];
pub fn wire_name(self) -> &'static str {
match self {
Self::ListDirectory => "list_directory",
Self::SearchDirectory => "search_directory",
Self::FindFile => "find_file",
Self::ViewFile => "view_file",
Self::CreateFile => "create_file",
Self::EditFile => "edit_file",
Self::DeleteFile => "delete_file",
Self::RenameFile => "rename_file",
Self::RunCommand => "run_command",
Self::AskQuestion => "ask_question",
Self::StartSubagent => "start_subagent",
Self::GenerateImage => "generate_image",
Self::CallAgent => "call_agent",
Self::CompileRustlite => "compile_rustlite",
Self::RunCartridge => "run_cartridge",
Self::RenderHtml => "render_html",
Self::ConfigureAgent => "configure_agent",
Self::CurrentTime => "current_time",
Self::Finish => "finish",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilitiesConfig {
pub enable_subagents: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled_tools: Option<Vec<BuiltinTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled_tools: Option<Vec<BuiltinTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compaction_threshold: Option<u32>,
pub image_model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_tool_schema_json: Option<String>,
}
impl Default for CapabilitiesConfig {
fn default() -> Self {
Self {
enable_subagents: true,
enabled_tools: Some(BuiltinTool::READ_ONLY.to_vec()),
disabled_tools: None,
compaction_threshold: None,
image_model: DEFAULT_IMAGE_GENERATION_MODEL.to_string(),
finish_tool_schema_json: None,
}
}
}
impl CapabilitiesConfig {
pub fn unrestricted() -> Self {
Self {
enable_subagents: true,
enabled_tools: None,
disabled_tools: None,
compaction_threshold: None,
image_model: DEFAULT_IMAGE_GENERATION_MODEL.to_string(),
finish_tool_schema_json: None,
}
}
pub fn effective_tools(&self) -> HashSet<BuiltinTool> {
match (&self.enabled_tools, &self.disabled_tools) {
(Some(en), _) => en.iter().copied().collect(),
(None, Some(dis)) => {
let disabled: HashSet<_> = dis.iter().copied().collect();
BuiltinTool::ALL
.iter()
.copied()
.filter(|t| !disabled.contains(t))
.collect()
}
(None, None) => BuiltinTool::ALL.iter().copied().collect(),
}
}
pub fn validate(&self) -> Result<(), crate::error::Error> {
if self.enabled_tools.is_some() && self.disabled_tools.is_some() {
return Err(crate::error::Error::config(
"enabled_tools and disabled_tools are mutually exclusive",
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpServerConfig {
Stdio {
command: String,
#[serde(default)]
args: Vec<String>,
},
Sse {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<std::collections::BTreeMap<String, String>>,
},
Http {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<std::collections::BTreeMap<String, String>>,
#[serde(default = "default_http_timeout")]
timeout_secs: f64,
#[serde(default = "default_sse_read_timeout")]
sse_read_timeout_secs: f64,
#[serde(default = "default_terminate_on_close")]
terminate_on_close: bool,
},
}
fn default_http_timeout() -> f64 {
30.0
}
fn default_sse_read_timeout() -> f64 {
300.0
}
fn default_terminate_on_close() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub name: String,
#[serde(default)]
pub args: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub canonical_path: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolResult {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub error: Option<String>,
}
impl ToolResult {
pub fn ok(name: impl Into<String>, id: Option<String>, value: serde_json::Value) -> Self {
Self {
name: name.into(),
id,
result: Some(value),
error: None,
}
}
pub fn err(name: impl Into<String>, id: Option<String>, message: impl Into<String>) -> Self {
Self {
name: name.into(),
id,
result: None,
error: Some(message.into()),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct UsageMetadata {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub prompt_token_count: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cached_content_token_count: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub candidates_token_count: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub thoughts_token_count: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub total_token_count: Option<i32>,
}
impl UsageMetadata {
pub fn merge_round(&mut self, other: &UsageMetadata) {
fn add(a: &mut Option<i32>, b: Option<i32>) {
if let Some(v) = b {
*a = Some(a.unwrap_or(0).saturating_add(v));
}
}
fn latest(a: &mut Option<i32>, b: Option<i32>) {
if b.is_some() {
*a = b;
}
}
latest(&mut self.prompt_token_count, other.prompt_token_count);
latest(
&mut self.cached_content_token_count,
other.cached_content_token_count,
);
add(
&mut self.candidates_token_count,
other.candidates_token_count,
);
add(&mut self.thoughts_token_count, other.thoughts_token_count);
add(&mut self.total_token_count, other.total_token_count);
}
pub fn accumulate(&mut self, other: &UsageMetadata) {
fn add(a: &mut Option<i32>, b: Option<i32>) {
if let Some(v) = b {
*a = Some(a.unwrap_or(0).saturating_add(v));
}
}
add(&mut self.prompt_token_count, other.prompt_token_count);
add(
&mut self.cached_content_token_count,
other.cached_content_token_count,
);
add(
&mut self.candidates_token_count,
other.candidates_token_count,
);
add(&mut self.thoughts_token_count, other.thoughts_token_count);
add(&mut self.total_token_count, other.total_token_count);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepType {
#[serde(rename = "TEXT_RESPONSE")]
TextResponse,
#[serde(rename = "TOOL_CALL")]
ToolCall,
#[serde(rename = "SYSTEM_MESSAGE")]
SystemMessage,
#[serde(rename = "COMPACTION")]
Compaction,
#[serde(rename = "FINISH")]
Finish,
#[serde(rename = "UNKNOWN")]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepSource {
#[serde(rename = "SYSTEM")]
System,
#[serde(rename = "USER")]
User,
#[serde(rename = "MODEL")]
Model,
#[serde(rename = "UNKNOWN")]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepTarget {
#[serde(rename = "TARGET_USER")]
User,
#[serde(rename = "TARGET_ENVIRONMENT")]
Environment,
#[serde(rename = "TARGET_UNSPECIFIED")]
Unspecified,
#[serde(rename = "UNKNOWN")]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepStatus {
#[serde(rename = "ACTIVE")]
Active,
#[serde(rename = "DONE")]
Done,
#[serde(rename = "WAITING_FOR_USER")]
WaitingForUser,
#[serde(rename = "ERROR")]
Error,
#[serde(rename = "CANCELED")]
Canceled,
#[serde(rename = "UNKNOWN")]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Step {
#[serde(default)]
pub id: String,
#[serde(default)]
pub step_index: u32,
#[serde(rename = "type", default = "Step::default_type")]
pub kind: StepType,
#[serde(default = "Step::default_source")]
pub source: StepSource,
#[serde(default = "Step::default_target")]
pub target: StepTarget,
#[serde(default = "Step::default_status")]
pub status: StepStatus,
#[serde(default)]
pub content: String,
#[serde(default)]
pub content_delta: String,
#[serde(default)]
pub thinking: String,
#[serde(default)]
pub thinking_delta: String,
#[serde(default)]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_results: Vec<ToolResult>,
#[serde(default)]
pub error: String,
#[serde(default)]
pub is_complete_response: Option<bool>,
#[serde(default)]
pub structured_output: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish_summary: Option<String>,
#[serde(default)]
pub usage_metadata: Option<UsageMetadata>,
}
impl Step {
fn default_type() -> StepType {
StepType::Unknown
}
fn default_source() -> StepSource {
StepSource::Unknown
}
fn default_target() -> StepTarget {
StepTarget::Unknown
}
fn default_status() -> StepStatus {
StepStatus::Unknown
}
fn base(kind: StepType, source: StepSource, target: StepTarget, status: StepStatus) -> Self {
Self {
id: String::new(),
step_index: 0,
kind,
source,
target,
status,
content: String::new(),
content_delta: String::new(),
thinking: String::new(),
thinking_delta: String::new(),
tool_calls: Vec::new(),
tool_results: Vec::new(),
error: String::new(),
is_complete_response: None,
structured_output: None,
finish_summary: None,
usage_metadata: None,
}
}
pub fn text_delta(trajectory_id: &str, step_index: u32, delta: &str) -> Self {
let mut s = Self::base(
StepType::TextResponse,
StepSource::Model,
StepTarget::User,
StepStatus::Active,
);
s.id = trajectory_id.to_string();
s.step_index = step_index;
s.content_delta = delta.to_string();
s.is_complete_response = Some(false);
s
}
pub fn thought_delta(trajectory_id: &str, step_index: u32, delta: &str) -> Self {
let mut s = Self::base(
StepType::TextResponse,
StepSource::Model,
StepTarget::User,
StepStatus::Active,
);
s.id = trajectory_id.to_string();
s.step_index = step_index;
s.thinking_delta = delta.to_string();
s.is_complete_response = Some(false);
s
}
pub fn tool_call(step_index: u32, call: ToolCall, status: StepStatus) -> Self {
let mut s = Self::base(
StepType::ToolCall,
StepSource::Model,
StepTarget::Environment,
status,
);
s.step_index = step_index;
s.tool_calls = vec![call];
s.is_complete_response = Some(false);
s
}
pub fn tool_result(step_index: u32, result: ToolResult) -> Self {
let mut s = Self::base(
StepType::ToolCall,
StepSource::Model,
StepTarget::Environment,
StepStatus::Done,
);
s.step_index = step_index;
s.error = result.error.clone().unwrap_or_default();
s.tool_results = vec![result];
s.is_complete_response = Some(false);
s
}
#[allow(clippy::too_many_arguments)] pub fn turn_complete(
trajectory_id: impl Into<String>,
step_index: u32,
status: StepStatus,
content: impl Into<String>,
error: impl Into<String>,
finished: bool,
structured_output: Option<serde_json::Value>,
usage_metadata: Option<UsageMetadata>,
) -> Self {
let kind = if finished || structured_output.is_some() {
StepType::Finish
} else {
StepType::TextResponse
};
let mut s = Self::base(kind, StepSource::Model, StepTarget::User, status);
s.id = trajectory_id.into();
s.step_index = step_index;
s.content = content.into();
s.error = error.into();
s.is_complete_response = Some(true);
s.structured_output = structured_output;
s.usage_metadata = usage_metadata;
s
}
pub fn with_finish_summary(mut self, summary: Option<String>) -> Self {
if self.kind == StepType::Finish {
self.finish_summary = summary.filter(|sm| !sm.is_empty());
}
self
}
pub fn turn_error(step_index: u32, message: impl Into<String>) -> Self {
let mut s = Self::base(
StepType::TextResponse,
StepSource::System,
StepTarget::User,
StepStatus::Error,
);
s.step_index = step_index;
s.error = message.into();
s.is_complete_response = Some(true);
s
}
pub fn is_terminal_response(&self) -> bool {
self.is_complete_response.unwrap_or(false)
|| (self.source == StepSource::Model
&& self.target == StepTarget::User
&& self.status == StepStatus::Done
&& !self.content.is_empty())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HookResult {
pub allow: bool,
#[serde(default)]
pub message: String,
}
impl HookResult {
pub fn allow() -> Self {
Self {
allow: true,
message: String::new(),
}
}
pub fn allow_with(message: impl Into<String>) -> Self {
Self {
allow: true,
message: message.into(),
}
}
pub fn deny(message: impl Into<String>) -> Self {
Self {
allow: false,
message: message.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AskQuestionOption {
pub id: String,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AskQuestionEntry {
pub question: String,
pub options: Vec<AskQuestionOption>,
#[serde(default)]
pub is_multi_select: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AskQuestionInteractionSpec {
pub questions: Vec<AskQuestionEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuestionResponse {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub selected_option_ids: Option<Vec<String>>,
#[serde(default)]
pub freeform_response: String,
#[serde(default)]
pub skipped: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TriggerDelivery {
SendImmediately,
#[default]
WaitIdle,
}
#[derive(Debug, Clone, PartialEq)]
pub enum StreamChunk {
Text {
step_index: u32,
text: String,
},
Thought {
step_index: u32,
text: String,
},
ToolCall(ToolCall),
ToolResult(ToolResult),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TranscriptRole {
User,
Assistant,
}
impl TranscriptRole {
pub fn as_str(&self) -> &'static str {
match self {
TranscriptRole::User => "user",
TranscriptRole::Assistant => "assistant",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TranscriptEntry {
pub role: TranscriptRole,
pub text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<TranscriptToolCall>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TranscriptToolCall {
pub name: String,
pub args: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[cfg(test)]
mod transcript_tests {
use super::*;
#[test]
fn old_format_entry_without_tool_calls_deserializes() {
let old = r#"{"role":"assistant","text":"hi there"}"#;
let entry: TranscriptEntry = serde_json::from_str(old).expect("old format must deserialize");
assert_eq!(entry.role, TranscriptRole::Assistant);
assert_eq!(entry.text, "hi there");
assert!(entry.tool_calls.is_empty(), "absent tool_calls → empty Vec");
}
#[test]
fn new_format_with_tool_calls_round_trips() {
let entry = TranscriptEntry {
role: TranscriptRole::Assistant,
text: "checking your files".into(),
tool_calls: vec![
TranscriptToolCall {
name: "view_file".into(),
args: serde_json::json!({"path": "main.rs"}),
result: Some(serde_json::json!({"contents": "fn main() {}"})),
error: None,
},
TranscriptToolCall {
name: "view_file".into(),
args: serde_json::json!({"path": "missing"}),
result: None,
error: Some("no such file".into()),
},
],
};
let bytes = serde_json::to_vec(&entry).unwrap();
let back: TranscriptEntry = serde_json::from_slice(&bytes).unwrap();
assert_eq!(entry, back);
let text_only = TranscriptEntry {
role: TranscriptRole::User,
text: "hello".into(),
tool_calls: Vec::new(),
};
let json = serde_json::to_string(&text_only).unwrap();
assert!(
!json.contains("tool_calls"),
"empty tool_calls must be omitted, got: {json}"
);
}
}