fix: correct inspector package name in Dockerfiles and add .dockerignore (#50)

* chore: remove inspect.sandboxagent.dev in favor of /ui/

* chore: add 404 page

* fix: correct inspector package name in Dockerfiles and add .dockerignore

- Change @anthropic-ai/sdk-inspector to @sandbox-agent/inspector in all Dockerfiles
- Add .dockerignore to exclude target/, node_modules/, etc from Docker context

The wrong package name caused pnpm install --filter to match nothing, so the
inspector frontend was never built, resulting in binaries without the /ui/ endpoint.

* chore: cargo fmt

* chore(release): update version to 0.1.4-rc.7
This commit is contained in:
Nathan Flurry 2026-02-01 23:03:51 -08:00 committed by GitHub
parent cacb63ef17
commit e3c030f66d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 927 additions and 771 deletions

View file

@ -4,20 +4,9 @@ use serde_json::Value;
use crate::amp as schema;
use crate::{
ContentPart,
ErrorData,
EventConversion,
ItemDeltaData,
ItemEventData,
ItemKind,
ItemRole,
ItemStatus,
SessionEndedData,
SessionEndReason,
TerminatedBy,
UniversalEventData,
UniversalEventType,
UniversalItem,
ContentPart, ErrorData, EventConversion, ItemDeltaData, ItemEventData, ItemKind, ItemRole,
ItemStatus, SessionEndReason, SessionEndedData, TerminatedBy, UniversalEventData,
UniversalEventType, UniversalItem,
};
static TEMP_ID: AtomicU64 = AtomicU64::new(1);
@ -27,7 +16,9 @@ fn next_temp_id(prefix: &str) -> String {
format!("{prefix}_{id}")
}
pub fn event_to_universal(event: &schema::StreamJsonMessage) -> Result<Vec<EventConversion>, String> {
pub fn event_to_universal(
event: &schema::StreamJsonMessage,
) -> Result<Vec<EventConversion>, String> {
let mut events = Vec::new();
match event.type_ {
schema::StreamJsonMessageType::Message => {
@ -49,12 +40,17 @@ pub fn event_to_universal(event: &schema::StreamJsonMessage) -> Result<Vec<Event
let arguments = match call.arguments {
schema::ToolCallArguments::Variant0(text) => text,
schema::ToolCallArguments::Variant1(map) => {
serde_json::to_string(&Value::Object(map)).unwrap_or_else(|_| "{}".to_string())
serde_json::to_string(&Value::Object(map))
.unwrap_or_else(|_| "{}".to_string())
}
};
(call.name, arguments, call.id)
} else {
("unknown".to_string(), "{}".to_string(), next_temp_id("tmp_amp_tool"))
(
"unknown".to_string(),
"{}".to_string(),
next_temp_id("tmp_amp_tool"),
)
};
let item = UniversalItem {
item_id: next_temp_id("tmp_amp_tool_call"),
@ -83,16 +79,16 @@ pub fn event_to_universal(event: &schema::StreamJsonMessage) -> Result<Vec<Event
parent_id: None,
kind: ItemKind::ToolResult,
role: Some(ItemRole::Tool),
content: vec![ContentPart::ToolResult {
call_id,
output,
}],
content: vec![ContentPart::ToolResult { call_id, output }],
status: ItemStatus::Completed,
};
events.extend(item_events(item));
}
schema::StreamJsonMessageType::Error => {
let message = event.error.clone().unwrap_or_else(|| "amp error".to_string());
let message = event
.error
.clone()
.unwrap_or_else(|| "amp error".to_string());
events.push(EventConversion::new(
UniversalEventType::Error,
UniversalEventData::Error(ErrorData {

View file

@ -3,21 +3,9 @@ use std::sync::atomic::{AtomicU64, Ordering};
use serde_json::Value;
use crate::{
ContentPart,
EventConversion,
ItemEventData,
ItemDeltaData,
ItemKind,
ItemRole,
ItemStatus,
PermissionEventData,
PermissionStatus,
QuestionEventData,
QuestionStatus,
SessionStartedData,
UniversalEventData,
UniversalEventType,
UniversalItem,
ContentPart, EventConversion, ItemDeltaData, ItemEventData, ItemKind, ItemRole, ItemStatus,
PermissionEventData, PermissionStatus, QuestionEventData, QuestionStatus, SessionStartedData,
UniversalEventData, UniversalEventType, UniversalItem,
};
static TEMP_ID: AtomicU64 = AtomicU64::new(1);
@ -56,8 +44,11 @@ fn system_event_to_universal(event: &Value) -> EventConversion {
let data = SessionStartedData {
metadata: Some(event.clone()),
};
EventConversion::new(UniversalEventType::SessionStarted, UniversalEventData::SessionStarted(data))
.with_raw(Some(event.clone()))
EventConversion::new(
UniversalEventType::SessionStarted,
UniversalEventData::SessionStarted(data),
)
.with_raw(Some(event.clone()))
}
fn assistant_event_to_universal(event: &Value, session_id: &str) -> Vec<EventConversion> {
@ -97,12 +88,15 @@ fn assistant_event_to_universal(event: &Value, session_id: &str) -> Vec<EventCon
);
let is_question_tool = matches!(
name,
"AskUserQuestion" | "ask_user_question" | "askUserQuestion"
"AskUserQuestion"
| "ask_user_question"
| "askUserQuestion"
| "ask-user-question"
) || is_exit_plan_mode;
let has_question_payload = input.get("questions").is_some();
if is_question_tool || has_question_payload {
if let Some(question) = question_from_claude_input(&input, call_id.clone()) {
if let Some(question) = question_from_claude_input(&input, call_id.clone())
{
conversions.push(
EventConversion::new(
UniversalEventType::QuestionRequested,
@ -117,10 +111,7 @@ fn assistant_event_to_universal(event: &Value, session_id: &str) -> Vec<EventCon
UniversalEventData::Question(QuestionEventData {
question_id: call_id.clone(),
prompt: "Approve plan execution?".to_string(),
options: vec![
"approve".to_string(),
"reject".to_string(),
],
options: vec!["approve".to_string(), "reject".to_string()],
response: None,
status: QuestionStatus::Requested,
}),
@ -129,7 +120,8 @@ fn assistant_event_to_universal(event: &Value, session_id: &str) -> Vec<EventCon
);
}
}
let arguments = serde_json::to_string(&input).unwrap_or_else(|_| "{}".to_string());
let arguments =
serde_json::to_string(&input).unwrap_or_else(|_| "{}".to_string());
let tool_item = UniversalItem {
item_id: String::new(),
native_item_id: Some(call_id.clone()),
@ -369,13 +361,12 @@ fn control_request_to_universal(event: &Value) -> Result<Vec<EventConversion>, S
.get("request")
.and_then(Value::as_object)
.ok_or_else(|| "missing request".to_string())?;
let subtype = request
.get("subtype")
.and_then(Value::as_str)
.unwrap_or("");
let subtype = request.get("subtype").and_then(Value::as_str).unwrap_or("");
if subtype != "can_use_tool" {
return Err(format!("unsupported Claude control_request subtype: {subtype}"));
return Err(format!(
"unsupported Claude control_request subtype: {subtype}"
));
}
let tool_name = request
@ -387,10 +378,7 @@ fn control_request_to_universal(event: &Value) -> Result<Vec<EventConversion>, S
.get("permission_suggestions")
.cloned()
.unwrap_or(Value::Null);
let blocked_path = request
.get("blocked_path")
.cloned()
.unwrap_or(Value::Null);
let blocked_path = request.get("blocked_path").cloned().unwrap_or(Value::Null);
let metadata = serde_json::json!({
"toolName": tool_name,

View file

@ -2,22 +2,9 @@ use serde_json::Value;
use crate::codex as schema;
use crate::{
ContentPart,
ErrorData,
EventConversion,
ItemDeltaData,
ItemEventData,
ItemKind,
ItemRole,
ItemStatus,
ReasoningVisibility,
SessionEndedData,
SessionEndReason,
SessionStartedData,
TerminatedBy,
UniversalEventData,
UniversalEventType,
UniversalItem,
ContentPart, ErrorData, EventConversion, ItemDeltaData, ItemEventData, ItemKind, ItemRole,
ItemStatus, ReasoningVisibility, SessionEndReason, SessionEndedData, SessionStartedData,
TerminatedBy, UniversalEventData, UniversalEventType, UniversalItem,
};
/// Convert a Codex ServerNotification to universal events.
@ -30,14 +17,12 @@ pub fn notification_to_universal(
let data = SessionStartedData {
metadata: serde_json::to_value(&params.thread).ok(),
};
Ok(vec![
EventConversion::new(
UniversalEventType::SessionStarted,
UniversalEventData::SessionStarted(data),
)
.with_native_session(Some(params.thread.id.clone()))
.with_raw(raw),
])
Ok(vec![EventConversion::new(
UniversalEventType::SessionStarted,
UniversalEventData::SessionStarted(data),
)
.with_native_session(Some(params.thread.id.clone()))
.with_raw(raw)])
}
schema::ServerNotification::ThreadCompacted(params) => Ok(vec![status_event(
"thread.compacted",
@ -77,28 +62,24 @@ pub fn notification_to_universal(
)]),
schema::ServerNotification::ItemStarted(params) => {
let item = thread_item_to_item(&params.item, ItemStatus::InProgress);
Ok(vec![
EventConversion::new(
UniversalEventType::ItemStarted,
UniversalEventData::Item(ItemEventData { item }),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw),
])
Ok(vec![EventConversion::new(
UniversalEventType::ItemStarted,
UniversalEventData::Item(ItemEventData { item }),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw)])
}
schema::ServerNotification::ItemCompleted(params) => {
let item = thread_item_to_item(&params.item, ItemStatus::Completed);
Ok(vec![
EventConversion::new(
UniversalEventType::ItemCompleted,
UniversalEventData::Item(ItemEventData { item }),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw),
])
Ok(vec![EventConversion::new(
UniversalEventType::ItemCompleted,
UniversalEventData::Item(ItemEventData { item }),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw)])
}
schema::ServerNotification::ItemAgentMessageDelta(params) => Ok(vec![
EventConversion::new(
schema::ServerNotification::ItemAgentMessageDelta(params) => {
Ok(vec![EventConversion::new(
UniversalEventType::ItemDelta,
UniversalEventData::ItemDelta(ItemDeltaData {
item_id: String::new(),
@ -107,10 +88,10 @@ pub fn notification_to_universal(
}),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw),
]),
schema::ServerNotification::ItemReasoningTextDelta(params) => Ok(vec![
EventConversion::new(
.with_raw(raw)])
}
schema::ServerNotification::ItemReasoningTextDelta(params) => {
Ok(vec![EventConversion::new(
UniversalEventType::ItemDelta,
UniversalEventData::ItemDelta(ItemDeltaData {
item_id: String::new(),
@ -119,10 +100,10 @@ pub fn notification_to_universal(
}),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw),
]),
schema::ServerNotification::ItemReasoningSummaryTextDelta(params) => Ok(vec![
EventConversion::new(
.with_raw(raw)])
}
schema::ServerNotification::ItemReasoningSummaryTextDelta(params) => {
Ok(vec![EventConversion::new(
UniversalEventType::ItemDelta,
UniversalEventData::ItemDelta(ItemDeltaData {
item_id: String::new(),
@ -131,10 +112,10 @@ pub fn notification_to_universal(
}),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw),
]),
schema::ServerNotification::ItemCommandExecutionOutputDelta(params) => Ok(vec![
EventConversion::new(
.with_raw(raw)])
}
schema::ServerNotification::ItemCommandExecutionOutputDelta(params) => {
Ok(vec![EventConversion::new(
UniversalEventType::ItemDelta,
UniversalEventData::ItemDelta(ItemDeltaData {
item_id: String::new(),
@ -143,10 +124,10 @@ pub fn notification_to_universal(
}),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw),
]),
schema::ServerNotification::ItemFileChangeOutputDelta(params) => Ok(vec![
EventConversion::new(
.with_raw(raw)])
}
schema::ServerNotification::ItemFileChangeOutputDelta(params) => {
Ok(vec![EventConversion::new(
UniversalEventType::ItemDelta,
UniversalEventData::ItemDelta(ItemDeltaData {
item_id: String::new(),
@ -155,10 +136,10 @@ pub fn notification_to_universal(
}),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw),
]),
schema::ServerNotification::ItemCommandExecutionTerminalInteraction(params) => Ok(vec![
EventConversion::new(
.with_raw(raw)])
}
schema::ServerNotification::ItemCommandExecutionTerminalInteraction(params) => {
Ok(vec![EventConversion::new(
UniversalEventType::ItemDelta,
UniversalEventData::ItemDelta(ItemDeltaData {
item_id: String::new(),
@ -167,33 +148,34 @@ pub fn notification_to_universal(
}),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw),
]),
.with_raw(raw)])
}
schema::ServerNotification::ItemMcpToolCallProgress(params) => Ok(vec![status_event(
"mcp.progress",
serde_json::to_string(params).ok(),
Some(params.thread_id.clone()),
raw,
)]),
schema::ServerNotification::ItemReasoningSummaryPartAdded(params) => Ok(vec![
status_event(
schema::ServerNotification::ItemReasoningSummaryPartAdded(params) => {
Ok(vec![status_event(
"reasoning.summary.part_added",
serde_json::to_string(params).ok(),
Some(params.thread_id.clone()),
raw,
),
]),
)])
}
schema::ServerNotification::Error(params) => {
let data = ErrorData {
message: params.error.message.clone(),
code: None,
details: serde_json::to_value(params).ok(),
};
Ok(vec![
EventConversion::new(UniversalEventType::Error, UniversalEventData::Error(data))
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw),
])
Ok(vec![EventConversion::new(
UniversalEventType::Error,
UniversalEventData::Error(data),
)
.with_native_session(Some(params.thread_id.clone()))
.with_raw(raw)])
}
schema::ServerNotification::RawResponseItemCompleted(params) => Ok(vec![status_event(
"raw.item.completed",
@ -239,7 +221,11 @@ fn thread_item_to_item(item: &schema::ThreadItem, status: ItemStatus) -> Univers
content: vec![ContentPart::Text { text: text.clone() }],
status,
},
schema::ThreadItem::Reasoning { content, id, summary } => {
schema::ThreadItem::Reasoning {
content,
id,
summary,
} => {
let mut parts = Vec::new();
for line in content {
parts.push(ContentPart::Reasoning {
@ -295,7 +281,11 @@ fn thread_item_to_item(item: &schema::ThreadItem, status: ItemStatus) -> Univers
status,
}
}
schema::ThreadItem::FileChange { changes, id, status: file_status } => UniversalItem {
schema::ThreadItem::FileChange {
changes,
id,
status: file_status,
} => UniversalItem {
item_id: String::new(),
native_item_id: Some(id.clone()),
parent_id: None,
@ -339,7 +329,8 @@ fn thread_item_to_item(item: &schema::ThreadItem, status: ItemStatus) -> Univers
output,
});
} else {
let arguments = serde_json::to_string(arguments).unwrap_or_else(|_| "{}".to_string());
let arguments =
serde_json::to_string(arguments).unwrap_or_else(|_| "{}".to_string());
parts.push(ContentPart::ToolCall {
name: format!("{server}.{tool}"),
arguments,
@ -433,18 +424,12 @@ fn thread_item_to_item(item: &schema::ThreadItem, status: ItemStatus) -> Univers
}],
status,
},
schema::ThreadItem::EnteredReviewMode { id, review } => status_item_internal(
id,
"review.entered",
Some(review.clone()),
status,
),
schema::ThreadItem::ExitedReviewMode { id, review } => status_item_internal(
id,
"review.exited",
Some(review.clone()),
status,
),
schema::ThreadItem::EnteredReviewMode { id, review } => {
status_item_internal(id, "review.entered", Some(review.clone()), status)
}
schema::ThreadItem::ExitedReviewMode { id, review } => {
status_item_internal(id, "review.exited", Some(review.clone()), status)
}
}
}
@ -463,7 +448,12 @@ fn status_item(label: &str, detail: Option<String>) -> UniversalItem {
}
}
fn status_item_internal(id: &str, label: &str, detail: Option<String>, status: ItemStatus) -> UniversalItem {
fn status_item_internal(
id: &str,
label: &str,
detail: Option<String>,
status: ItemStatus,
) -> UniversalItem {
UniversalItem {
item_id: String::new(),
native_item_id: Some(id.to_string()),

View file

@ -2,46 +2,42 @@ use serde_json::Value;
use crate::opencode as schema;
use crate::{
ContentPart,
EventConversion,
ItemDeltaData,
ItemEventData,
ItemKind,
ItemRole,
ItemStatus,
PermissionEventData,
PermissionStatus,
QuestionEventData,
QuestionStatus,
SessionStartedData,
UniversalEventData,
UniversalEventType,
UniversalItem,
ContentPart, EventConversion, ItemDeltaData, ItemEventData, ItemKind, ItemRole, ItemStatus,
PermissionEventData, PermissionStatus, QuestionEventData, QuestionStatus, SessionStartedData,
UniversalEventData, UniversalEventType, UniversalItem,
};
pub fn event_to_universal(event: &schema::Event) -> Result<Vec<EventConversion>, String> {
let raw = serde_json::to_value(event).ok();
match event {
schema::Event::MessageUpdated(updated) => {
let schema::EventMessageUpdated { properties, type_: _ } = updated;
let schema::EventMessageUpdated {
properties,
type_: _,
} = updated;
let schema::EventMessageUpdatedProperties { info } = properties;
let (mut item, completed, session_id) = message_to_item(info);
item.status = if completed { ItemStatus::Completed } else { ItemStatus::InProgress };
item.status = if completed {
ItemStatus::Completed
} else {
ItemStatus::InProgress
};
let event_type = if completed {
UniversalEventType::ItemCompleted
} else {
UniversalEventType::ItemStarted
};
let conversion = EventConversion::new(
event_type,
UniversalEventData::Item(ItemEventData { item }),
)
.with_native_session(session_id)
.with_raw(raw);
let conversion =
EventConversion::new(event_type, UniversalEventData::Item(ItemEventData { item }))
.with_native_session(session_id)
.with_raw(raw);
Ok(vec![conversion])
}
schema::Event::MessagePartUpdated(updated) => {
let schema::EventMessagePartUpdated { properties, type_: _ } = updated;
let schema::EventMessagePartUpdated {
properties,
type_: _,
} = updated;
let schema::EventMessagePartUpdatedProperties { part, delta } = properties;
let mut events = Vec::new();
let (session_id, message_id) = part_session_message(part);
@ -122,11 +118,16 @@ pub fn event_to_universal(event: &schema::Event) -> Result<Vec<EventConversion>,
schema::Part::Variant4(tool_part) => {
let tool_events = tool_part_to_events(&tool_part, &message_id);
for event in tool_events {
events.push(event.with_native_session(session_id.clone()).with_raw(raw.clone()));
events.push(
event
.with_native_session(session_id.clone())
.with_raw(raw.clone()),
);
}
}
schema::Part::Variant1 { .. } => {
let detail = serde_json::to_string(part).unwrap_or_else(|_| "subtask".to_string());
let detail =
serde_json::to_string(part).unwrap_or_else(|_| "subtask".to_string());
let item = status_item("subtask", Some(detail));
events.push(
EventConversion::new(
@ -160,7 +161,10 @@ pub fn event_to_universal(event: &schema::Event) -> Result<Vec<EventConversion>,
Ok(events)
}
schema::Event::QuestionAsked(asked) => {
let schema::EventQuestionAsked { properties, type_: _ } = asked;
let schema::EventQuestionAsked {
properties,
type_: _,
} = asked;
let question = question_from_opencode(properties);
let conversion = EventConversion::new(
UniversalEventType::QuestionRequested,
@ -171,7 +175,10 @@ pub fn event_to_universal(event: &schema::Event) -> Result<Vec<EventConversion>,
Ok(vec![conversion])
}
schema::Event::PermissionAsked(asked) => {
let schema::EventPermissionAsked { properties, type_: _ } = asked;
let schema::EventPermissionAsked {
properties,
type_: _,
} = asked;
let permission = permission_from_opencode(properties);
let conversion = EventConversion::new(
UniversalEventType::PermissionRequested,
@ -182,7 +189,10 @@ pub fn event_to_universal(event: &schema::Event) -> Result<Vec<EventConversion>,
Ok(vec![conversion])
}
schema::Event::SessionCreated(created) => {
let schema::EventSessionCreated { properties, type_: _ } = created;
let schema::EventSessionCreated {
properties,
type_: _,
} = created;
let metadata = serde_json::to_value(&properties.info).ok();
let conversion = EventConversion::new(
UniversalEventType::SessionStarted,
@ -193,8 +203,12 @@ pub fn event_to_universal(event: &schema::Event) -> Result<Vec<EventConversion>,
Ok(vec![conversion])
}
schema::Event::SessionStatus(status) => {
let schema::EventSessionStatus { properties, type_: _ } = status;
let detail = serde_json::to_string(&properties.status).unwrap_or_else(|_| "status".to_string());
let schema::EventSessionStatus {
properties,
type_: _,
} = status;
let detail =
serde_json::to_string(&properties.status).unwrap_or_else(|_| "status".to_string());
let item = status_item("session.status", Some(detail));
let conversion = EventConversion::new(
UniversalEventType::ItemCompleted,
@ -205,7 +219,10 @@ pub fn event_to_universal(event: &schema::Event) -> Result<Vec<EventConversion>,
Ok(vec![conversion])
}
schema::Event::SessionIdle(idle) => {
let schema::EventSessionIdle { properties, type_: _ } = idle;
let schema::EventSessionIdle {
properties,
type_: _,
} = idle;
let item = status_item("session.idle", None);
let conversion = EventConversion::new(
UniversalEventType::ItemCompleted,
@ -216,8 +233,12 @@ pub fn event_to_universal(event: &schema::Event) -> Result<Vec<EventConversion>,
Ok(vec![conversion])
}
schema::Event::SessionError(error) => {
let schema::EventSessionError { properties, type_: _ } = error;
let detail = serde_json::to_string(&properties.error).unwrap_or_else(|_| "session error".to_string());
let schema::EventSessionError {
properties,
type_: _,
} = error;
let detail = serde_json::to_string(&properties.error)
.unwrap_or_else(|_| "session error".to_string());
let item = status_item("session.error", Some(detail));
let conversion = EventConversion::new(
UniversalEventType::ItemCompleted,
@ -270,7 +291,11 @@ fn message_to_item(message: &schema::Message) -> (UniversalItem, bool, Option<St
kind: ItemKind::Message,
role: Some(ItemRole::Assistant),
content: Vec::new(),
status: if completed { ItemStatus::Completed } else { ItemStatus::InProgress },
status: if completed {
ItemStatus::Completed
} else {
ItemStatus::InProgress
},
},
completed,
Some(session_id.clone()),
@ -281,9 +306,10 @@ fn message_to_item(message: &schema::Message) -> (UniversalItem, bool, Option<St
fn part_session_message(part: &schema::Part) -> (Option<String>, String) {
match part {
schema::Part::Variant0(text_part) => {
(Some(text_part.session_id.clone()), text_part.message_id.clone())
}
schema::Part::Variant0(text_part) => (
Some(text_part.session_id.clone()),
text_part.message_id.clone(),
),
schema::Part::Variant1 {
session_id,
message_id,

View file

@ -1,13 +1,16 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use schemars::JsonSchema;
use utoipa::ToSchema;
pub use sandbox_agent_extracted_agent_schemas::{amp, claude, codex, opencode};
pub mod agents;
pub use agents::{amp as convert_amp, claude as convert_claude, codex as convert_codex, opencode as convert_opencode};
pub use agents::{
amp as convert_amp, claude as convert_claude, codex as convert_codex,
opencode as convert_opencode,
};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct UniversalEvent {
@ -221,14 +224,38 @@ pub enum ItemStatus {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text { text: String },
Json { json: Value },
ToolCall { name: String, arguments: String, call_id: String },
ToolResult { call_id: String, output: String },
FileRef { path: String, action: FileAction, diff: Option<String> },
Reasoning { text: String, visibility: ReasoningVisibility },
Image { path: String, mime: Option<String> },
Status { label: String, detail: Option<String> },
Text {
text: String,
},
Json {
json: Value,
},
ToolCall {
name: String,
arguments: String,
call_id: String,
},
ToolResult {
call_id: String,
output: String,
},
FileRef {
path: String,
action: FileAction,
diff: Option<String>,
},
Reasoning {
text: String,
visibility: ReasoningVisibility,
},
Image {
path: String,
mime: Option<String>,
},
Status {
label: String,
detail: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]