use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::BTreeMap;
pub(crate) mod flex_num {
use serde::{Deserialize, Deserializer, de::Error};
use serde_json::Value;
fn value_to_i64<E: Error>(value: &Value) -> Result<i64, E> {
match value {
Value::Number(n) => n
.as_i64()
.ok_or_else(|| E::custom(format!("number {n} does not fit in i64"))),
Value::String(s) => s
.parse::<i64>()
.map_err(|e| E::custom(format!("invalid i64 string {s:?}: {e}"))),
other => Err(E::custom(format!("expected i64, got {other}"))),
}
}
fn value_to_u64<E: Error>(value: &Value) -> Result<u64, E> {
match value {
Value::Number(n) => n
.as_u64()
.ok_or_else(|| E::custom(format!("number {n} does not fit in u64"))),
Value::String(s) => s
.parse::<u64>()
.map_err(|e| E::custom(format!("invalid u64 string {s:?}: {e}"))),
other => Err(E::custom(format!("expected u64, got {other}"))),
}
}
pub fn opt_u64<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u64>, D::Error> {
match Option::<Value>::deserialize(d)? {
None | Some(Value::Null) => Ok(None),
Some(v) => value_to_u64(&v).map(Some),
}
}
pub fn opt_u32<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u32>, D::Error> {
match Option::<Value>::deserialize(d)? {
None | Some(Value::Null) => Ok(None),
Some(v) => {
let raw = value_to_u64(&v)?;
u32::try_from(raw)
.map(Some)
.map_err(|_| Error::custom(format!("value {raw} does not fit in u32")))
}
}
}
pub fn opt_i32<'de, D: Deserializer<'de>>(d: D) -> Result<Option<i32>, D::Error> {
match Option::<Value>::deserialize(d)? {
None | Some(Value::Null) => Ok(None),
Some(v) => {
let raw = value_to_i64(&v)?;
i32::try_from(raw)
.map(Some)
.map_err(|_| Error::custom(format!("value {raw} does not fit in i32")))
}
}
}
}
macro_rules! wire_string_enum {
(
$(#[$meta:meta])*
$name:ident, $ctx:ident, $unknown_type_fn:ident {
$( $(#[$vmeta:meta])* $variant:ident => $wire:literal ),+ $(,)?
}
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum $name {
$( $(#[$vmeta])* $variant, )+
Unknown {
$ctx: String,
data: Value,
},
}
impl $name {
#[must_use]
pub fn as_wire_str(&self) -> &str {
match self {
$( Self::$variant => $wire, )+
Self::Unknown { $ctx, .. } => $ctx,
}
}
#[must_use]
pub const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown { .. })
}
#[must_use]
pub fn $unknown_type_fn(&self) -> Option<&str> {
match self {
Self::Unknown { $ctx, .. } => Some($ctx),
_ => None,
}
}
#[must_use]
pub fn unknown_data(&self) -> Option<&Value> {
match self {
Self::Unknown { data, .. } => Some(data),
_ => None,
}
}
}
impl Serialize for $name {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_wire_str())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = Value::deserialize(deserializer)?;
if let Value::String(s) = &value {
match s.as_str() {
$( $wire => return Ok(Self::$variant), )+
_ => {}
}
}
let $ctx = match &value {
Value::String(s) => s.clone(),
other => other.to_string(),
};
tracing::warn!(
concat!("Unknown ", stringify!($name), " wire value: '{}'. \
Preserving in Unknown variant."),
$ctx
);
Ok(Self::Unknown { $ctx, data: value })
}
}
};
}
wire_string_enum!(
StepState, state_type, unknown_state_type {
Unspecified => "STATE_UNSPECIFIED",
Active => "STATE_ACTIVE",
Done => "STATE_DONE",
WaitingForUser => "STATE_WAITING_FOR_USER",
Error => "STATE_ERROR",
}
);
wire_string_enum!(
StepSource, source_type, unknown_source_type {
Unspecified => "SOURCE_UNSPECIFIED",
System => "SOURCE_SYSTEM",
User => "SOURCE_USER",
Model => "SOURCE_MODEL",
}
);
wire_string_enum!(
StepTarget, target_type, unknown_target_type {
Unspecified => "TARGET_UNSPECIFIED",
User => "TARGET_USER",
Model => "TARGET_MODEL",
Environment => "TARGET_ENVIRONMENT",
}
);
wire_string_enum!(
TrajectoryState, state_type, unknown_state_type {
Unspecified => "STATE_UNSPECIFIED",
Running => "STATE_RUNNING",
Idle => "STATE_IDLE",
Cancelled => "STATE_CANCELLED",
}
);
wire_string_enum!(
ModelType, model_type, unknown_model_type {
Unspecified => "MODEL_TYPE_UNSPECIFIED",
Text => "MODEL_TYPE_TEXT",
Image => "MODEL_TYPE_IMAGE",
}
);
wire_string_enum!(
LifecycleHook, hook_type, unknown_hook_type {
Unspecified => "LIFECYCLE_HOOK_UNSPECIFIED",
OnSessionStart => "LIFECYCLE_HOOK_ON_SESSION_START",
OnSessionEnd => "LIFECYCLE_HOOK_ON_SESSION_END",
PreTurn => "LIFECYCLE_HOOK_PRE_TURN",
PostTurn => "LIFECYCLE_HOOK_POST_TURN",
PreTool => "LIFECYCLE_HOOK_PRE_TOOL",
PostTool => "LIFECYCLE_HOOK_POST_TOOL",
OnToolError => "LIFECYCLE_HOOK_ON_TOOL_ERROR",
}
);
wire_string_enum!(
HookDecision, decision_type, unknown_decision_type {
Unspecified => "DECISION_UNSPECIFIED",
Allow => "ALLOW",
Deny => "DENY",
}
);
wire_string_enum!(
LineAction, action_type, unknown_action_type {
Unspecified => "LINE_ACTION_UNSPECIFIED",
Insert => "LINE_ACTION_INSERT",
Delete => "LINE_ACTION_DELETE",
None => "LINE_ACTION_NONE",
}
);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct InitializeConversationEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<HarnessConfig>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct HarnessConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub cascade_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_instructions: Option<SystemInstructions>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub tools: Vec<Tool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub harness_side_tools: Option<HarnessSideTools>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compaction_threshold: Option<u32>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub workspaces: Vec<Workspace>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub skills_paths: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_tool_schema_json: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_trajectory: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub app_data_dir: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub mcp_servers: Vec<McpServerConfig>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub models: Vec<ModelConfig>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub enabled_hooks: Vec<LifecycleHook>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub custom_subagents: Vec<CustomAgent>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SystemInstructions {
#[serde(skip_serializing_if = "Option::is_none")]
pub custom: Option<CustomSystemInstructions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub appended: Option<AppendedSystemInstructions>,
}
impl SystemInstructions {
#[must_use]
pub fn custom_text(text: impl Into<String>) -> Self {
Self {
custom: Some(CustomSystemInstructions {
part: vec![SystemInstructionPart {
text: Some(text.into()),
}],
}),
appended: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CustomSystemInstructions {
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub part: Vec<SystemInstructionPart>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SystemInstructionPart {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct AppendedSystemInstructions {
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_identity: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub appended_sections: Vec<InstructionSection>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct InstructionSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Tool {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters_json_schema: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_json_schema: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ToolToggle {
pub enabled: bool,
}
impl ToolToggle {
#[must_use]
pub const fn new(enabled: bool) -> Self {
Self { enabled }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct HarnessSideTools {
#[serde(skip_serializing_if = "Option::is_none")]
pub find: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_command: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subagents: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_questions: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_edit: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub view_file: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub write_to_file: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grep_search: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub list_dir: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<PermissionsConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub generate_image: Option<ToolToggle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_web: Option<ToolToggle>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PermissionsConfig {
pub enforce_workspace_validation: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Workspace {
#[serde(skip_serializing_if = "Option::is_none")]
pub filesystem_workspace: Option<FilesystemWorkspace>,
}
impl Workspace {
#[must_use]
pub fn filesystem(directory: impl Into<String>) -> Self {
Self {
filesystem_workspace: Some(FilesystemWorkspace {
directory: Some(directory.into()),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct FilesystemWorkspace {
#[serde(skip_serializing_if = "Option::is_none")]
pub directory: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ModelConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub types: Vec<ModelType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gemini_api_endpoint: Option<GeminiApiEndpoint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vertex_endpoint: Option<VertexEndpoint>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct GeminiApiEndpoint {
#[serde(skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub http_headers: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<GeminiModelOptions>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct VertexEndpoint {
#[serde(skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub http_headers: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<GeminiModelOptions>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct GeminiModelOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_level: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stdio: Option<McpStdioTransport>,
#[serde(skip_serializing_if = "Option::is_none")]
pub http: Option<McpHttpTransport>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub enabled_tools: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub disabled_tools: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout_seconds: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct McpStdioTransport {
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub args: Vec<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub env: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct McpHttpTransport {
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub headers: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CustomAgent {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_instructions: Option<SystemInstructions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub harness_side_tools: Option<HarnessSideTools>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub tools: Vec<Tool>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum InputEvent {
UserInput(String),
ComplexUserInput(UserInput),
ToolConfirmation(ToolConfirmation),
ToolResponse(ToolResponse),
QuestionResponse(UserQuestionsResponse),
HaltRequest(bool),
AutomatedTrigger(String),
CallHookResponse(CallHookResponse),
SessionEndRequest(bool),
Unknown {
event_type: String,
data: Value,
},
}
impl InputEvent {
#[must_use]
pub const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown { .. })
}
#[must_use]
pub fn unknown_event_type(&self) -> Option<&str> {
match self {
Self::Unknown { event_type, .. } => Some(event_type),
_ => None,
}
}
#[must_use]
pub fn unknown_data(&self) -> Option<&Value> {
match self {
Self::Unknown { data, .. } => Some(data),
_ => None,
}
}
fn oneof_key(&self) -> &str {
match self {
Self::UserInput(_) => "userInput",
Self::ComplexUserInput(_) => "complexUserInput",
Self::ToolConfirmation(_) => "toolConfirmation",
Self::ToolResponse(_) => "toolResponse",
Self::QuestionResponse(_) => "questionResponse",
Self::HaltRequest(_) => "haltRequest",
Self::AutomatedTrigger(_) => "automatedTrigger",
Self::CallHookResponse(_) => "callHookResponse",
Self::SessionEndRequest(_) => "sessionEndRequest",
Self::Unknown { event_type, .. } => event_type,
}
}
fn oneof_value(&self) -> Result<Value, serde_json::Error> {
match self {
Self::UserInput(s) | Self::AutomatedTrigger(s) => Ok(Value::String(s.clone())),
Self::ComplexUserInput(v) => serde_json::to_value(v),
Self::ToolConfirmation(v) => serde_json::to_value(v),
Self::ToolResponse(v) => serde_json::to_value(v),
Self::QuestionResponse(v) => serde_json::to_value(v),
Self::HaltRequest(b) | Self::SessionEndRequest(b) => Ok(Value::Bool(*b)),
Self::CallHookResponse(v) => serde_json::to_value(v),
Self::Unknown { data, .. } => Ok(data.clone()),
}
}
}
impl Serialize for InputEvent {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let value = self.oneof_value().map_err(serde::ser::Error::custom)?;
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry(self.oneof_key(), &value)?;
map.end()
}
}
impl<'de> Deserialize<'de> for InputEvent {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
let map = Map::deserialize(deserializer)?;
let (key, value) = map
.into_iter()
.next()
.ok_or_else(|| D::Error::custom("InputEvent must have exactly one field set"))?;
let event = match key.as_str() {
"userInput" => Self::UserInput(
value
.as_str()
.ok_or_else(|| D::Error::custom("userInput must be a string"))?
.to_string(),
),
"complexUserInput" => {
Self::ComplexUserInput(serde_json::from_value(value).map_err(D::Error::custom)?)
}
"toolConfirmation" => {
Self::ToolConfirmation(serde_json::from_value(value).map_err(D::Error::custom)?)
}
"toolResponse" => {
Self::ToolResponse(serde_json::from_value(value).map_err(D::Error::custom)?)
}
"questionResponse" => {
Self::QuestionResponse(serde_json::from_value(value).map_err(D::Error::custom)?)
}
"haltRequest" => Self::HaltRequest(value.as_bool().unwrap_or_default()),
"automatedTrigger" => Self::AutomatedTrigger(
value
.as_str()
.ok_or_else(|| D::Error::custom("automatedTrigger must be a string"))?
.to_string(),
),
"callHookResponse" => {
Self::CallHookResponse(serde_json::from_value(value).map_err(D::Error::custom)?)
}
"sessionEndRequest" => Self::SessionEndRequest(value.as_bool().unwrap_or_default()),
_ => Self::Unknown {
event_type: key,
data: value,
},
};
Ok(event)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UserInput {
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub parts: Vec<UserInputPart>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UserInputPart {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<Media>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slash_command: Option<SlashCommand>,
}
impl UserInputPart {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self {
text: Some(text.into()),
..Self::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Media {
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SlashCommand {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ToolConfirmation {
pub trajectory_id: String,
pub step_index: u32,
pub accepted: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ToolResponse {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_json: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub supplemental_media: Vec<Media>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UserQuestionsResponse {
pub trajectory_id: String,
pub step_index: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub cancelled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response: Option<QuestionsResponse>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct QuestionsResponse {
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub answers: Vec<UserQuestionAnswer>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UserQuestionAnswer {
#[serde(skip_serializing_if = "Option::is_none")]
pub unanswered: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_choice_answer: Option<MultipleChoiceAnswer>,
}
impl UserQuestionAnswer {
#[must_use]
pub fn unanswered() -> Self {
Self {
unanswered: Some(true),
multiple_choice_answer: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct MultipleChoiceAnswer {
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub selected_choice_indices: Vec<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub freeform_response: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CallHookResponse {
pub request_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_turn_result: Option<HookVerdict>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_tool_result: Option<HookVerdict>,
#[serde(skip_serializing_if = "Option::is_none")]
pub empty_result: Option<EmptyResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct HookVerdict {
#[serde(skip_serializing_if = "Option::is_none")]
pub decision: Option<HookDecision>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct EmptyResult {}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct OutputEvent {
pub seq_num: Option<i64>,
pub timestamp_micros: Option<i64>,
pub usage_metadata: Option<UsageMetadata>,
pub payload: Option<OutputPayload>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum OutputPayload {
StepUpdate(Box<StepUpdate>),
TrajectoryStateUpdate(TrajectoryStateUpdate),
ToolCall(ToolCall),
InitializeConversationResponse(InitializeConversationResponse),
CallHookRequest(CallHookRequest),
SessionEndResponse(bool),
Unknown {
event_type: String,
data: Value,
},
}
impl OutputPayload {
#[must_use]
pub const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown { .. })
}
#[must_use]
pub fn unknown_event_type(&self) -> Option<&str> {
match self {
Self::Unknown { event_type, .. } => Some(event_type),
_ => None,
}
}
#[must_use]
pub fn unknown_data(&self) -> Option<&Value> {
match self {
Self::Unknown { data, .. } => Some(data),
_ => None,
}
}
}
const OUTPUT_PAYLOAD_KEYS: &[&str] = &[
"stepUpdate",
"trajectoryStateUpdate",
"toolCall",
"initializeConversationResponse",
"callHookRequest",
"sessionEndResponse",
];
impl Serialize for OutputEvent {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::{Error, SerializeMap};
let mut len = 0;
len += usize::from(self.seq_num.is_some());
len += usize::from(self.timestamp_micros.is_some());
len += usize::from(self.usage_metadata.is_some());
len += usize::from(self.payload.is_some());
let mut map = serializer.serialize_map(Some(len))?;
if let Some(seq_num) = self.seq_num {
map.serialize_entry("seqNum", &seq_num)?;
}
if let Some(timestamp_micros) = self.timestamp_micros {
map.serialize_entry("timestampMicros", ×tamp_micros)?;
}
if let Some(payload) = &self.payload {
let (key, value) = match payload {
OutputPayload::StepUpdate(v) => (
"stepUpdate",
serde_json::to_value(v).map_err(S::Error::custom)?,
),
OutputPayload::TrajectoryStateUpdate(v) => (
"trajectoryStateUpdate",
serde_json::to_value(v).map_err(S::Error::custom)?,
),
OutputPayload::ToolCall(v) => (
"toolCall",
serde_json::to_value(v).map_err(S::Error::custom)?,
),
OutputPayload::InitializeConversationResponse(v) => (
"initializeConversationResponse",
serde_json::to_value(v).map_err(S::Error::custom)?,
),
OutputPayload::CallHookRequest(v) => (
"callHookRequest",
serde_json::to_value(v).map_err(S::Error::custom)?,
),
OutputPayload::SessionEndResponse(b) => ("sessionEndResponse", Value::Bool(*b)),
OutputPayload::Unknown { event_type, data } => (event_type.as_str(), data.clone()),
};
map.serialize_entry(key, &value)?;
}
if let Some(usage_metadata) = &self.usage_metadata {
map.serialize_entry("usageMetadata", usage_metadata)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for OutputEvent {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
let mut map = Map::deserialize(deserializer)?;
fn take_i64<E: Error>(map: &mut Map<String, Value>, key: &str) -> Result<Option<i64>, E> {
match map.remove(key) {
None | Some(Value::Null) => Ok(None),
Some(Value::Number(n)) => n
.as_i64()
.map(Some)
.ok_or_else(|| E::custom(format!("{key}: number does not fit in i64"))),
Some(Value::String(s)) => s
.parse::<i64>()
.map(Some)
.map_err(|e| E::custom(format!("{key}: invalid i64 string: {e}"))),
Some(other) => Err(E::custom(format!("{key}: expected i64, got {other}"))),
}
}
let seq_num = take_i64(&mut map, "seqNum")?;
let timestamp_micros = take_i64(&mut map, "timestampMicros")?;
let usage_metadata = match map.remove("usageMetadata") {
None | Some(Value::Null) => None,
Some(v) => Some(serde_json::from_value(v).map_err(D::Error::custom)?),
};
let mut payload = None;
for key in OUTPUT_PAYLOAD_KEYS {
if let Some(value) = map.remove(*key) {
payload = Some(match *key {
"stepUpdate" => OutputPayload::StepUpdate(Box::new(
serde_json::from_value(value).map_err(D::Error::custom)?,
)),
"trajectoryStateUpdate" => OutputPayload::TrajectoryStateUpdate(
serde_json::from_value(value).map_err(D::Error::custom)?,
),
"toolCall" => OutputPayload::ToolCall(
serde_json::from_value(value).map_err(D::Error::custom)?,
),
"initializeConversationResponse" => {
OutputPayload::InitializeConversationResponse(
serde_json::from_value(value).map_err(D::Error::custom)?,
)
}
"callHookRequest" => OutputPayload::CallHookRequest(
serde_json::from_value(value).map_err(D::Error::custom)?,
),
"sessionEndResponse" => {
OutputPayload::SessionEndResponse(value.as_bool().unwrap_or_default())
}
_ => unreachable!("key list is exhaustive"),
});
break;
}
}
if payload.is_none()
&& let Some((event_type, data)) = map.into_iter().next()
{
tracing::warn!(
"Unknown OutputEvent variant: '{}'. Preserving in Unknown variant.",
event_type
);
payload = Some(OutputPayload::Unknown { event_type, data });
}
Ok(Self {
seq_num,
timestamp_micros,
usage_metadata,
payload,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct InitializeConversationResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub cascade_id: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub history: Vec<StepUpdate>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UsageMetadata {
#[serde(
default,
deserialize_with = "flex_num::opt_u64",
skip_serializing_if = "Option::is_none"
)]
pub prompt_token_count: Option<u64>,
#[serde(
default,
deserialize_with = "flex_num::opt_u64",
skip_serializing_if = "Option::is_none"
)]
pub cached_content_token_count: Option<u64>,
#[serde(
default,
deserialize_with = "flex_num::opt_u64",
skip_serializing_if = "Option::is_none"
)]
pub candidates_token_count: Option<u64>,
#[serde(
default,
deserialize_with = "flex_num::opt_u64",
skip_serializing_if = "Option::is_none"
)]
pub thoughts_token_count: Option<u64>,
#[serde(
default,
deserialize_with = "flex_num::opt_u64",
skip_serializing_if = "Option::is_none"
)]
pub total_token_count: Option<u64>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct StepUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub cascade_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trajectory_id: Option<String>,
#[serde(
default,
deserialize_with = "flex_num::opt_u32",
skip_serializing_if = "Option::is_none"
)]
pub step_index: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<StepState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<StepSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<StepTarget>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_delta: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_delta: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub list_directory: Option<ActionListDirectory>,
#[serde(skip_serializing_if = "Option::is_none")]
pub find_file: Option<ActionFindFile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_directory: Option<ActionSearchDirectory>,
#[serde(skip_serializing_if = "Option::is_none")]
pub view_file: Option<ActionViewFile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub create_file: Option<ActionCreateFile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edit_file: Option<ActionEditFile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_command: Option<ActionRunCommand>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compaction: Option<ActionCompaction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub invoke_subagent: Option<ActionInvokeSubagent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub generate_image: Option<ActionGenerateImage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish: Option<ActionFinish>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ActionError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcp_tool: Option<ActionMcpTool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_web: Option<ActionSearchWeb>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_confirmation_request: Option<ToolConfirmationRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub questions_request: Option<UserQuestionsRequest>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ToolConfirmationRequest {
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UserQuestionsRequest {
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub questions: Vec<UserQuestion>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UserQuestion {
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_choice: Option<MultipleChoice>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct MultipleChoice {
#[serde(skip_serializing_if = "Option::is_none")]
pub question: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub choices: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_multi_select: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TrajectoryStateUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub trajectory_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<TrajectoryState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ToolCall {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments_json: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CallHookRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub hook_type: Option<LifecycleHook>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_turn_args: Option<PreTurnArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_turn_args: Option<PostTurnArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_tool_args: Option<PreToolArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_tool_args: Option<PostToolArgs>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PreToolArgs {
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments_json: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PostToolArgs {
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PreTurnArgs {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_input: Option<UserInput>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PostTurnArgs {
#[serde(skip_serializing_if = "Option::is_none")]
pub response_text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionListDirectory {
#[serde(skip_serializing_if = "Option::is_none")]
pub directory_path: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub results: Vec<ListDirectoryEntry>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ListDirectoryEntry {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_directory: Option<bool>,
#[serde(
default,
deserialize_with = "flex_num::opt_u64",
skip_serializing_if = "Option::is_none"
)]
pub file_size: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionFindFile {
#[serde(skip_serializing_if = "Option::is_none")]
pub directory_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionSearchDirectory {
#[serde(skip_serializing_if = "Option::is_none")]
pub directory_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(
default,
deserialize_with = "flex_num::opt_i32",
skip_serializing_if = "Option::is_none"
)]
pub num_results: Option<i32>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionViewFile {
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
#[serde(
default,
deserialize_with = "flex_num::opt_u32",
skip_serializing_if = "Option::is_none"
)]
pub start_line: Option<u32>,
#[serde(
default,
deserialize_with = "flex_num::opt_u32",
skip_serializing_if = "Option::is_none"
)]
pub end_line: Option<u32>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionCreateFile {
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contents: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionEditFile {
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub diff_block: Vec<DiffBlock>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct DiffBlock {
#[serde(
default,
deserialize_with = "flex_num::opt_i32",
skip_serializing_if = "Option::is_none"
)]
pub start_line: Option<i32>,
#[serde(
default,
deserialize_with = "flex_num::opt_i32",
skip_serializing_if = "Option::is_none"
)]
pub end_line: Option<i32>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub lines: Vec<DiffLine>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct DiffLine {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<LineAction>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionRunCommand {
#[serde(skip_serializing_if = "Option::is_none")]
pub command_line: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub working_dir: Option<String>,
#[serde(
default,
deserialize_with = "flex_num::opt_i32",
skip_serializing_if = "Option::is_none"
)]
pub exit_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub combined_output: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionCompaction {
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionInvokeSubagent {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionGenerateImage {
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub image_paths: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionFinish {
#[serde(skip_serializing_if = "Option::is_none")]
pub output_string: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionError {
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
#[serde(
default,
deserialize_with = "flex_num::opt_u32",
skip_serializing_if = "Option::is_none"
)]
pub http_code: Option<u32>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionMcpTool {
#[serde(skip_serializing_if = "Option::is_none")]
pub server_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments_json: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ActionSearchWeb {
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_input_event_user_input_golden() {
let event = InputEvent::UserInput("hello".to_string());
assert_eq!(
serde_json::to_value(&event).unwrap(),
json!({"userInput": "hello"})
);
}
#[test]
fn test_input_event_tool_response_golden() {
let event = InputEvent::ToolResponse(ToolResponse {
id: "1".to_string(),
response_json: Some(r#"{"a":1}"#.to_string()),
supplemental_media: vec![],
});
assert_eq!(
serde_json::to_value(&event).unwrap(),
json!({"toolResponse": {"id": "1", "responseJson": "{\"a\":1}"}})
);
}
#[test]
fn test_input_event_halt_request_golden() {
let event = InputEvent::HaltRequest(true);
assert_eq!(
serde_json::to_value(&event).unwrap(),
json!({"haltRequest": true})
);
}
#[test]
fn test_input_event_tool_confirmation_golden() {
let event = InputEvent::ToolConfirmation(ToolConfirmation {
trajectory_id: "t".to_string(),
step_index: 2,
accepted: true,
});
assert_eq!(
serde_json::to_value(&event).unwrap(),
json!({"toolConfirmation": {"trajectoryId": "t", "stepIndex": 2, "accepted": true}})
);
}
#[test]
fn test_input_event_call_hook_response_golden() {
let event = InputEvent::CallHookResponse(CallHookResponse {
request_id: "r".to_string(),
pre_tool_result: Some(HookVerdict {
decision: Some(HookDecision::Allow),
reason: Some("ok".to_string()),
}),
..Default::default()
});
assert_eq!(
serde_json::to_value(&event).unwrap(),
json!({"callHookResponse": {"requestId": "r", "preToolResult": {"decision": "ALLOW", "reason": "ok"}}})
);
}
#[test]
fn test_initialize_conversation_event_golden() {
let event = InitializeConversationEvent {
config: Some(HarnessConfig {
cascade_id: Some("cid".to_string()),
system_instructions: Some(SystemInstructions::custom_text("hi")),
tools: vec![Tool {
name: Some("t".to_string()),
description: Some("d".to_string()),
parameters_json_schema: Some("{}".to_string()),
response_json_schema: Some("{}".to_string()),
}],
harness_side_tools: Some(HarnessSideTools {
run_command: Some(ToolToggle::new(false)),
view_file: Some(ToolToggle::new(true)),
..Default::default()
}),
workspaces: vec![Workspace::filesystem("/w")],
mcp_servers: vec![McpServerConfig {
name: Some("git".to_string()),
stdio: Some(McpStdioTransport {
command: Some("uvx".to_string()),
args: vec!["x".to_string()],
env: BTreeMap::from([("K".to_string(), "V".to_string())]),
}),
..Default::default()
}],
models: vec![ModelConfig {
name: Some("gemini-3-flash-preview".to_string()),
types: vec![ModelType::Text],
gemini_api_endpoint: Some(GeminiApiEndpoint {
api_key: Some("k".to_string()),
http_headers: BTreeMap::from([("a".to_string(), "b".to_string())]),
..Default::default()
}),
..Default::default()
}],
enabled_hooks: vec![LifecycleHook::PreTool],
..Default::default()
}),
};
let expected = json!({"config": {
"cascadeId": "cid",
"systemInstructions": {"custom": {"part": [{"text": "hi"}]}},
"tools": [{"name": "t", "description": "d", "parametersJsonSchema": "{}", "responseJsonSchema": "{}"}],
"harnessSideTools": {"runCommand": {"enabled": false}, "viewFile": {"enabled": true}},
"workspaces": [{"filesystemWorkspace": {"directory": "/w"}}],
"mcpServers": [{"name": "git", "stdio": {"command": "uvx", "args": ["x"], "env": {"K": "V"}}}],
"models": [{"name": "gemini-3-flash-preview", "types": ["MODEL_TYPE_TEXT"], "geminiApiEndpoint": {"httpHeaders": {"a": "b"}, "apiKey": "k"}}],
"enabledHooks": ["LIFECYCLE_HOOK_PRE_TOOL"],
}});
assert_eq!(serde_json::to_value(&event).unwrap(), expected);
}
#[test]
fn test_output_event_step_update_golden() {
let raw = r#"{"seqNum": "12345678901234", "timestampMicros": "2", "stepUpdate": {"trajectoryId": "traj", "stepIndex": 3, "state": "STATE_DONE", "source": "SOURCE_MODEL", "target": "TARGET_USER", "text": "hello", "runCommand": {"commandLine": "ls", "exitCode": 0, "combinedOutput": "a\n"}}, "usageMetadata": {"promptTokenCount": "10", "totalTokenCount": "20"}}"#;
let event: OutputEvent = serde_json::from_str(raw).unwrap();
assert_eq!(event.seq_num, Some(12_345_678_901_234));
assert_eq!(event.timestamp_micros, Some(2));
let usage = event.usage_metadata.as_ref().unwrap();
assert_eq!(usage.prompt_token_count, Some(10));
assert_eq!(usage.total_token_count, Some(20));
let Some(OutputPayload::StepUpdate(step)) = &event.payload else {
panic!("expected StepUpdate, got {:?}", event.payload);
};
assert_eq!(step.trajectory_id.as_deref(), Some("traj"));
assert_eq!(step.step_index, Some(3));
assert_eq!(step.state, Some(StepState::Done));
assert_eq!(step.source, Some(StepSource::Model));
assert_eq!(step.target, Some(StepTarget::User));
assert_eq!(step.text.as_deref(), Some("hello"));
let run = step.run_command.as_ref().unwrap();
assert_eq!(run.command_line.as_deref(), Some("ls"));
assert_eq!(run.exit_code, Some(0));
assert_eq!(run.combined_output.as_deref(), Some("a\n"));
}
#[test]
fn test_output_event_tool_call_golden() {
let raw = r#"{"seqNum": "1", "toolCall": {"id": "abc", "name": "get_weather", "argumentsJson": "{\"city\":\"SF\"}"}}"#;
let event: OutputEvent = serde_json::from_str(raw).unwrap();
let Some(OutputPayload::ToolCall(call)) = &event.payload else {
panic!("expected ToolCall");
};
assert_eq!(call.id.as_deref(), Some("abc"));
assert_eq!(call.name.as_deref(), Some("get_weather"));
assert_eq!(call.arguments_json.as_deref(), Some(r#"{"city":"SF"}"#));
}
#[test]
fn test_output_event_init_response_golden() {
let raw = r#"{"initializeConversationResponse": {"cascadeId": "cid"}}"#;
let event: OutputEvent = serde_json::from_str(raw).unwrap();
let Some(OutputPayload::InitializeConversationResponse(resp)) = &event.payload else {
panic!("expected InitializeConversationResponse");
};
assert_eq!(resp.cascade_id.as_deref(), Some("cid"));
assert!(resp.history.is_empty());
}
fn roundtrip_output(event: &OutputEvent) -> OutputEvent {
let json = serde_json::to_string(event).unwrap();
serde_json::from_str(&json).unwrap()
}
fn roundtrip_input(event: &InputEvent) -> InputEvent {
let json = serde_json::to_string(event).unwrap();
serde_json::from_str(&json).unwrap()
}
#[test]
fn test_input_event_roundtrip_all_variants() {
let events = vec![
InputEvent::UserInput("hi".to_string()),
InputEvent::ComplexUserInput(UserInput {
parts: vec![
UserInputPart::text("a"),
UserInputPart {
media: Some(Media {
mime_type: Some("image/png".to_string()),
description: Some("d".to_string()),
data: Some("aGVsbG8=".to_string()),
}),
..Default::default()
},
UserInputPart {
slash_command: Some(SlashCommand {
name: Some("compact".to_string()),
}),
..Default::default()
},
],
}),
InputEvent::ToolConfirmation(ToolConfirmation {
trajectory_id: "t".to_string(),
step_index: 7,
accepted: false,
}),
InputEvent::ToolResponse(ToolResponse {
id: "id".to_string(),
response_json: Some("{}".to_string()),
supplemental_media: vec![Media::default()],
}),
InputEvent::QuestionResponse(UserQuestionsResponse {
trajectory_id: "t".to_string(),
step_index: 1,
cancelled: None,
response: Some(QuestionsResponse {
answers: vec![
UserQuestionAnswer::unanswered(),
UserQuestionAnswer {
multiple_choice_answer: Some(MultipleChoiceAnswer {
selected_choice_indices: vec![0, 2],
freeform_response: Some("f".to_string()),
}),
..Default::default()
},
],
}),
}),
InputEvent::HaltRequest(true),
InputEvent::AutomatedTrigger("tick".to_string()),
InputEvent::CallHookResponse(CallHookResponse {
request_id: "r".to_string(),
empty_result: Some(EmptyResult {}),
..Default::default()
}),
InputEvent::SessionEndRequest(true),
InputEvent::Unknown {
event_type: "futureEvent".to_string(),
data: json!({"x": 1}),
},
];
for event in events {
assert_eq!(roundtrip_input(&event), event);
}
}
#[test]
fn test_input_event_unknown_variant_preserves_wire_format() {
let raw = r#"{"futureEvent": {"payload": 42}}"#;
let event: InputEvent = serde_json::from_str(raw).unwrap();
assert!(event.is_unknown());
assert_eq!(event.unknown_event_type(), Some("futureEvent"));
assert_eq!(event.unknown_data(), Some(&json!({"payload": 42})));
assert_eq!(
serde_json::to_value(&event).unwrap(),
json!({"futureEvent": {"payload": 42}})
);
}
#[test]
fn test_output_event_roundtrip_all_variants() {
let events = vec![
OutputEvent {
seq_num: Some(1),
timestamp_micros: Some(2),
usage_metadata: Some(UsageMetadata {
prompt_token_count: Some(1),
total_token_count: Some(2),
..Default::default()
}),
payload: Some(OutputPayload::StepUpdate(Box::new(StepUpdate {
trajectory_id: Some("t".to_string()),
step_index: Some(1),
state: Some(StepState::Active),
text_delta: Some("d".to_string()),
..Default::default()
}))),
},
OutputEvent {
payload: Some(OutputPayload::TrajectoryStateUpdate(
TrajectoryStateUpdate {
trajectory_id: Some("t".to_string()),
state: Some(TrajectoryState::Idle),
error: None,
extra: Map::new(),
},
)),
..Default::default()
},
OutputEvent {
payload: Some(OutputPayload::ToolCall(ToolCall {
id: Some("i".to_string()),
name: Some("n".to_string()),
arguments_json: Some("{}".to_string()),
..Default::default()
})),
..Default::default()
},
OutputEvent {
payload: Some(OutputPayload::InitializeConversationResponse(
InitializeConversationResponse {
cascade_id: Some("c".to_string()),
history: vec![StepUpdate::default()],
extra: Map::new(),
},
)),
..Default::default()
},
OutputEvent {
payload: Some(OutputPayload::CallHookRequest(CallHookRequest {
request_id: Some("r".to_string()),
hook_type: Some(LifecycleHook::PreTool),
pre_tool_args: Some(PreToolArgs {
tool_name: Some("run_command".to_string()),
arguments_json: Some("{}".to_string()),
}),
..Default::default()
})),
..Default::default()
},
OutputEvent {
payload: Some(OutputPayload::SessionEndResponse(true)),
..Default::default()
},
OutputEvent {
payload: Some(OutputPayload::Unknown {
event_type: "newThing".to_string(),
data: json!({"a": [1, 2]}),
}),
..Default::default()
},
OutputEvent::default(),
];
for event in events {
assert_eq!(roundtrip_output(&event), event);
}
}
#[test]
fn test_output_event_unknown_variant_preserved() {
let raw = r#"{"seqNum": "5", "brandNewEvent": {"k": "v"}}"#;
let event: OutputEvent = serde_json::from_str(raw).unwrap();
assert_eq!(event.seq_num, Some(5));
let payload = event.payload.as_ref().unwrap();
assert!(payload.is_unknown());
assert_eq!(payload.unknown_event_type(), Some("brandNewEvent"));
assert_eq!(payload.unknown_data(), Some(&json!({"k": "v"})));
let json = serde_json::to_value(&event).unwrap();
assert_eq!(json["brandNewEvent"], json!({"k": "v"}));
assert_eq!(json["seqNum"], json!(5));
}
#[test]
fn test_step_update_unknown_fields_preserved() {
let raw = r#"{"trajectoryId": "t", "futureField": {"nested": true}}"#;
let step: StepUpdate = serde_json::from_str(raw).unwrap();
assert_eq!(
step.extra.get("futureField"),
Some(&json!({"nested": true}))
);
let json = serde_json::to_value(&step).unwrap();
assert_eq!(json["futureField"], json!({"nested": true}));
}
#[test]
fn test_wire_enum_unknown_value_preserved() {
let state: StepState = serde_json::from_value(json!("STATE_HIBERNATING")).unwrap();
assert!(state.is_unknown());
assert_eq!(state.unknown_state_type(), Some("STATE_HIBERNATING"));
assert_eq!(state.unknown_data(), Some(&json!("STATE_HIBERNATING")));
assert_eq!(
serde_json::to_value(&state).unwrap(),
json!("STATE_HIBERNATING")
);
}
#[test]
fn test_wire_enum_known_values_roundtrip() {
for (value, wire) in [
(StepState::Unspecified, "STATE_UNSPECIFIED"),
(StepState::Active, "STATE_ACTIVE"),
(StepState::Done, "STATE_DONE"),
(StepState::WaitingForUser, "STATE_WAITING_FOR_USER"),
(StepState::Error, "STATE_ERROR"),
] {
assert_eq!(serde_json::to_value(&value).unwrap(), json!(wire));
let parsed: StepState = serde_json::from_value(json!(wire)).unwrap();
assert_eq!(parsed, value);
assert!(!parsed.is_unknown());
assert_eq!(parsed.as_wire_str(), wire);
}
}
#[test]
fn test_usage_metadata_accepts_numbers_and_strings() {
let from_strings: UsageMetadata =
serde_json::from_value(json!({"promptTokenCount": "7", "totalTokenCount": "9"}))
.unwrap();
let from_numbers: UsageMetadata =
serde_json::from_value(json!({"promptTokenCount": 7, "totalTokenCount": 9})).unwrap();
assert_eq!(from_strings.prompt_token_count, Some(7));
assert_eq!(from_strings, from_numbers);
}
#[test]
fn test_questions_request_deserializes() {
let raw = r#"{"questions": [{"multipleChoice": {"question": "q?", "choices": ["a", "b"], "isMultiSelect": true}}, {"holographicQuestion": {"q": 1}}]}"#;
let req: UserQuestionsRequest = serde_json::from_str(raw).unwrap();
assert_eq!(req.questions.len(), 2);
let mc = req.questions[0].multiple_choice.as_ref().unwrap();
assert_eq!(mc.question.as_deref(), Some("q?"));
assert_eq!(mc.choices, vec!["a", "b"]);
assert_eq!(mc.is_multi_select, Some(true));
assert!(req.questions[1].multiple_choice.is_none());
assert!(req.questions[1].extra.contains_key("holographicQuestion"));
}
#[test]
fn test_edit_file_diff_structure() {
let raw = r#"{"filePath": "/f", "diffBlock": [{"startLine": 1, "endLine": 2, "lines": [{"text": "x", "action": "LINE_ACTION_INSERT"}]}]}"#;
let action: ActionEditFile = serde_json::from_str(raw).unwrap();
assert_eq!(action.file_path.as_deref(), Some("/f"));
assert_eq!(
action.diff_block[0].lines[0].action,
Some(LineAction::Insert)
);
}
#[test]
fn test_tool_call_arguments_struct_preserved() {
let raw = r#"{"id": "1", "name": "n", "arguments": {"fields": [{"name": "city", "value": {"stringValue": "SF"}}]}}"#;
let call: ToolCall = serde_json::from_str(raw).unwrap();
assert!(call.arguments.is_some());
let json = serde_json::to_value(&call).unwrap();
assert_eq!(json["arguments"]["fields"][0]["name"], "city");
}
#[test]
fn test_flex_num_rejects_garbage() {
let result: Result<UsageMetadata, _> =
serde_json::from_value(json!({"promptTokenCount": "not-a-number"}));
assert!(result.is_err());
let result: Result<UsageMetadata, _> =
serde_json::from_value(json!({"promptTokenCount": true}));
assert!(result.is_err());
}
#[test]
fn test_flex_num_null_is_none() {
let usage: UsageMetadata =
serde_json::from_value(json!({"promptTokenCount": null})).unwrap();
assert_eq!(usage.prompt_token_count, None);
}
#[test]
fn test_output_event_seq_num_string_and_number() {
let a: OutputEvent = serde_json::from_str(r#"{"seqNum": "3"}"#).unwrap();
let b: OutputEvent = serde_json::from_str(r#"{"seqNum": 3}"#).unwrap();
assert_eq!(a.seq_num, Some(3));
assert_eq!(a, b);
}
}