pub mod text { use std::collections::HashSet; use std::fmt; use std::io::{self, Write}; const CANCELLED_MESSAGE: &str = ""; use ai::agent::action_result::{FetchConversationResult, ReadSkillResult, UseComputerResult}; use itertools::Itertools; use crate::ai::agent::{ AIAgentActionType, AIAgentInput, AIAgentOutput, AIAgentOutputMessageType, AIAgentTodo, ArtifactCreatedData, CallMCPToolResult, FileGlobResult, FileGlobV2Result, GrepResult, ReadFilesResult, ReadMCPResourceResult, RequestCommandOutputResult, RequestFileEditsResult, SearchCodebaseResult, SuggestNewConversationResult, SuggestPromptResult, TodoOperation, UploadArtifactResult, WebFetchStatus, WebSearchStatus, WriteToLongRunningShellCommandResult, }; use crate::AIAgentActionResultType; /// Format an agent input as a human-readable string. For action results, it's assumed that /// the action is shown immediately before this result. /// /// Unlike other contexts where we format agent inputs, this is a user-facing API. Consider /// what details are relevant and acceptable to expose. pub fn format_input(input: &AIAgentInput, w: &mut W) -> io::Result<()> { match input { AIAgentInput::UserQuery { .. } | AIAgentInput::CommandCompletionAssessment { .. } | AIAgentInput::AutoCodeDiffQuery { .. } | AIAgentInput::CreateNewProject { .. } | AIAgentInput::CloneRepository { .. } | AIAgentInput::InitProjectRules { .. } | AIAgentInput::CodeReview { .. } | AIAgentInput::FetchReviewComments { .. } | AIAgentInput::CreateEnvironment { .. } | AIAgentInput::SummarizeConversation { .. } | AIAgentInput::InvokeSkill { .. } | AIAgentInput::StartFromAmbientRunPrompt { .. } | AIAgentInput::MessagesReceivedFromAgents { .. } | AIAgentInput::PassiveSuggestionResult { .. } | AIAgentInput::EventsFromAgents { .. } | AIAgentInput::OrchestrationConfigUpdate { .. } => { // Do not include the user query, since it's already provided as input to the agent. Ok(()) } // These input types should not occur in a SDK-run agent. AIAgentInput::ResumeConversation { .. } | AIAgentInput::TriggerPassiveSuggestion { .. } => Ok(()), AIAgentInput::ActionResult { result, .. } => match &result.result { AIAgentActionResultType::RequestCommandOutput(result) => match result { RequestCommandOutputResult::Completed { command, output, exit_code, .. } => writeln!(w, "{output}\n\n (`{command}` exited with code {exit_code})"), RequestCommandOutputResult::LongRunningCommandSnapshot { command, .. } => { writeln!(w, "`{command}` is still running...") } RequestCommandOutputResult::CancelledBeforeExecution => { writeln!(w, "{CANCELLED_MESSAGE}") } RequestCommandOutputResult::ExecutionError { command, message } => { writeln!(w, "Command `{command}` was not executed: {message}") } RequestCommandOutputResult::Denylisted { .. } => { writeln!( w, "Command was not allowed to run due to presence on denylist" ) } }, AIAgentActionResultType::WriteToLongRunningShellCommand(result) => match result { WriteToLongRunningShellCommandResult::Snapshot { .. } => { writeln!(w, "Command is still running...") } WriteToLongRunningShellCommandResult::CommandFinished { output, exit_code, .. } => writeln!(w, "{output}\n\n (exited with code {exit_code})"), WriteToLongRunningShellCommandResult::Cancelled => { writeln!(w, "{CANCELLED_MESSAGE}") } WriteToLongRunningShellCommandResult::Error(_) => { writeln!(w, "Failed to write to command.") } }, AIAgentActionResultType::RequestFileEdits(result) => match result { RequestFileEditsResult::Success { diff, updated_files, deleted_files, .. } => { writeln!( w, "Updated {} files, deleted {} files:\n```diff\n{diff}\n```", updated_files.len(), deleted_files.len() ) } RequestFileEditsResult::Cancelled => { writeln!(w, "{CANCELLED_MESSAGE}") } RequestFileEditsResult::DiffApplicationFailed { error } => { writeln!(w, "Editing files failed: {error}") } }, AIAgentActionResultType::ReadFiles(result) => match result { ReadFilesResult::Success { .. } => Ok(()), ReadFilesResult::Error(error) => writeln!(w, "Reading files failed: {error}"), ReadFilesResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::UploadArtifact(result) => match result { UploadArtifactResult::Success { artifact_uid, filepath, .. } => match filepath { Some(filepath) => { writeln!(w, "Uploaded artifact {artifact_uid} from {filepath}") } None => writeln!(w, "Uploaded artifact {artifact_uid}"), }, UploadArtifactResult::Error(error) => { writeln!(w, "Uploading artifact failed: {error}") } UploadArtifactResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::SearchCodebase(result) => match result { SearchCodebaseResult::Success { files } => { writeln!(w, "Codebase search results:")?; for file in files { writeln!(w, "- {file}")?; } Ok(()) } SearchCodebaseResult::Failed { message, .. } => { writeln!(w, "Searching codebase failed: {message}") } SearchCodebaseResult::Cancelled => todo!(), }, AIAgentActionResultType::Grep(result) => match result { GrepResult::Success { matched_files } => { for file in matched_files { writeln!(w, "- {file}")?; } Ok(()) } GrepResult::Error(error) => writeln!(w, "grep failed: {error}"), GrepResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::FileGlob(result) => match result { FileGlobResult::Success { matched_files } => writeln!(w, "{matched_files}"), FileGlobResult::Error(error) => writeln!(w, "find failed: {error}"), FileGlobResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::FileGlobV2(result) => match result { FileGlobV2Result::Success { matched_files, .. } => { for file in matched_files { writeln!(w, "- {file}")?; } Ok(()) } FileGlobV2Result::Error(error) => writeln!(w, "find failed: {error}"), FileGlobV2Result::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::ReadMCPResource(result) => match result { ReadMCPResourceResult::Success { resource_contents } => { for resource in resource_contents { write!(w, "- ")?; match resource { rmcp::model::ResourceContents::TextResourceContents { uri, mime_type, text, .. } => writeln!( w, "{uri} ({})\n{text}", mime_type.as_deref().unwrap_or("text/plain") )?, rmcp::model::ResourceContents::BlobResourceContents { uri, mime_type, .. } => writeln!( w, "{uri} ({})", mime_type.as_deref().unwrap_or("text/plain") )?, } } Ok(()) } ReadMCPResourceResult::Error(error) => { writeln!(w, "Reading MCP resource failed: {error}") } ReadMCPResourceResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::CallMCPTool(result) => { match result { CallMCPToolResult::Success { result } => { for content in &result.content { write!(w, "- ")?; match &content.raw { rmcp::model::RawContent::Text(text_content) => { writeln!(w, "{}", text_content.text)?; } rmcp::model::RawContent::Image(image_content) => { writeln!(w, "{} image", image_content.mime_type)?; } rmcp::model::RawContent::Resource(embedded_resource) => { match &embedded_resource.resource { rmcp::model::ResourceContents::TextResourceContents { uri, mime_type, text, .. } => { writeln!(w, "{uri} ({})\n{text}", mime_type.as_deref().unwrap_or("text/plain"))?; } rmcp::model::ResourceContents::BlobResourceContents { uri, mime_type, .. } => { writeln!(w, "{uri} ({})", mime_type.as_deref().unwrap_or("text/plain"))?; } }; } rmcp::model::RawContent::Audio(audio_content) => { writeln!(w, "{} audio", audio_content.mime_type)?; } rmcp::model::RawContent::ResourceLink(raw_resource) => { let rmcp::model::RawResource { uri, mime_type, name, .. } = raw_resource; writeln!( w, "{name}: {uri} ({})", mime_type.as_deref().unwrap_or("unknown") )?; } } } Ok(()) } CallMCPToolResult::Error(error) => { writeln!(w, "Calling MCP tool failed: {error}") } CallMCPToolResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), } } AIAgentActionResultType::ReadSkill(result) => match result { ReadSkillResult::Success { content } => { writeln!(w, "Skill read successfully: {}", content.file_name) } ReadSkillResult::Error(error) => writeln!(w, "Skill read error: {error}"), ReadSkillResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::SuggestNewConversation(result) => match result { SuggestNewConversationResult::Accepted { .. } | SuggestNewConversationResult::Rejected => Ok(()), SuggestNewConversationResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::SuggestPrompt(result) => match result { SuggestPromptResult::Accepted { .. } => Ok(()), SuggestPromptResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, AIAgentActionResultType::OpenCodeReview => Ok(()), AIAgentActionResultType::InsertReviewComments(_) => Ok(()), AIAgentActionResultType::InitProject => Ok(()), // Document operations - not yet implemented for SDK AIAgentActionResultType::ReadDocuments(_) | AIAgentActionResultType::EditDocuments(_) | AIAgentActionResultType::CreateDocuments(_) => Ok(()), AIAgentActionResultType::ReadShellCommandOutput { .. } => Ok(()), AIAgentActionResultType::TransferShellCommandControlToUser { .. } => Ok(()), AIAgentActionResultType::UseComputer(result) => match result { // TODO(AGENT-2281): implement UseComputerResult::Success(_result) => Ok(()), UseComputerResult::Error(error) => writeln!(w, "Use computer error: {error}"), UseComputerResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, // TODO(AGENT-2281): implement AIAgentActionResultType::RequestComputerUse(_result) => Ok(()), AIAgentActionResultType::FetchConversation(result) => match result { FetchConversationResult::Success { directory_path } => { writeln!(w, "Fetched conversation to {directory_path}") } FetchConversationResult::Error(error) => { writeln!(w, "Fetch conversation error: {error}") } FetchConversationResult::Cancelled => writeln!(w, "{CANCELLED_MESSAGE}"), }, // StartAgent is a client-side orchestration action, not used in SDK AIAgentActionResultType::StartAgent(_) => Ok(()), // SendMessageToAgent is a client-side orchestration action, not used in SDK AIAgentActionResultType::SendMessageToAgent(_) => Ok(()), AIAgentActionResultType::AskUserQuestion(_) => Ok(()), // RunAgents is a desktop-client-only action; not used in the SDK. AIAgentActionResultType::RunAgents(_) => Ok(()), // No user-visible payload to emit. AIAgentActionResultType::WaitForEvents(_) => Ok(()), }, } } pub fn format_output(output: &AIAgentOutput, w: &mut W) -> io::Result<()> { for message in output.messages.iter() { match &message.message { AIAgentOutputMessageType::Text(text) | AIAgentOutputMessageType::Reasoning { text, .. } | AIAgentOutputMessageType::Summarization { text, .. } => { super::format_agent_text(text, w)?; } AIAgentOutputMessageType::Action(action) => match &action.action { AIAgentActionType::RequestCommandOutput { command, .. } => { writeln!(w, "Running `{command}`")?; } AIAgentActionType::WriteToLongRunningShellCommand { input, .. } => { writeln!(w, "Write {} bytes to command", input.len())?; } AIAgentActionType::ReadFiles(request) => { writeln!( w, "Reading {}", request .locations .iter() .format_with(", ", |loc, f| f(&format_args!("{}", loc.name))) )?; // TODO: Better formatting, need shell info. } AIAgentActionType::UploadArtifact(request) => { writeln!(w, "Uploading artifact {}", request.file_path)?; } AIAgentActionType::SearchCodebase(request) => { writeln!( w, "Searching {} for {}", request.codebase_path.as_deref().unwrap_or("codebase"), request.query )?; } AIAgentActionType::RequestFileEdits { file_edits, title } => { write!(w, "Editing files:")?; if let Some(title) = title { write!(w, " {title}")?; } writeln!(w)?; let file_paths: HashSet<_> = file_edits.iter().flat_map(|edit| edit.file()).collect(); for path in file_paths { writeln!(w, "- {path}")?; } } AIAgentActionType::Grep { queries, path } => { writeln!(w, "Grepping for {} in {path}", format_queries(queries))?; } AIAgentActionType::FileGlob { patterns, path } => { write!(w, "Finding files matching {}", format_queries(patterns))?; if let Some(path) = path { write!(w, " in {path}")?; } writeln!(w)?; } AIAgentActionType::FileGlobV2 { patterns, search_dir, } => { write!(w, "Finding files matching {}", format_queries(patterns))?; if let Some(path) = search_dir { write!(w, " in {path}")?; } writeln!(w)?; } AIAgentActionType::ReadMCPResource { server_id: _, name, uri, } => match uri { Some(uri) => writeln!(w, "Reading MCP resource {uri}")?, None => writeln!(w, "Reading MCP resource {name}")?, }, AIAgentActionType::CallMCPTool { server_id: _, name, input, } => { writeln!(w, "MCP tool call {name}({input:#})")?; } AIAgentActionType::SuggestNewConversation { .. } => (), AIAgentActionType::SuggestPrompt { .. } => (), AIAgentActionType::OpenCodeReview => (), AIAgentActionType::InsertCodeReviewComments { .. } => (), AIAgentActionType::InitProject => (), // Document operations - not yet implemented for SDK AIAgentActionType::ReadDocuments(_) | AIAgentActionType::EditDocuments(_) | AIAgentActionType::CreateDocuments(_) | AIAgentActionType::ReadShellCommandOutput { .. } | AIAgentActionType::TransferShellCommandControlToUser { .. } => (), AIAgentActionType::UseComputer(request) => { writeln!(w, "Computer use action: {}", request.action_summary)?; } AIAgentActionType::RequestComputerUse(request) => { writeln!(w, "Requesting computer use: {}", request.task_summary)?; } AIAgentActionType::ReadSkill(request) => { writeln!(w, "Reading skill: {}", request.skill)?; } AIAgentActionType::FetchConversation { conversation_id } => { writeln!(w, "Fetching conversation {conversation_id}")?; } AIAgentActionType::StartAgent { name, .. } => { writeln!(w, "Starting agent: {name}")?; } AIAgentActionType::SendMessageToAgent { addresses, subject, .. } => { writeln!( w, "Sending message to [{}]: {subject}", addresses.join(", ") )?; } AIAgentActionType::AskUserQuestion { .. } => (), // RunAgents is desktop-client-only; SDK driver renders nothing. AIAgentActionType::RunAgents(_) => (), AIAgentActionType::WaitForEvents { .. } => (), }, AIAgentOutputMessageType::RuntimeActivity(activity) => { writeln!(w, "{}", activity.title)?; if let Some(output) = &activity.output { writeln!(w, "{output}")?; } } AIAgentOutputMessageType::TodoOperation(operation) => match operation { TodoOperation::UpdateTodos { todos } => { writeln!(w, "Updated TODO list:")?; format_todos(todos, w)?; } TodoOperation::MarkAsCompleted { completed_todos } => { writeln!(w, "Completed TODOs:")?; format_todos(completed_todos, w)?; } }, AIAgentOutputMessageType::Subagent(subagent) => { writeln!(w, "{subagent}")?; } AIAgentOutputMessageType::WebSearch(status) => match status { WebSearchStatus::Searching { query } => match query { Some(q) => writeln!(w, "Searching web for: {q}")?, None => writeln!(w, "Searching web")?, }, WebSearchStatus::Success { query, pages } => { writeln!(w, "Searched web for: {query} ({} results)", pages.len())?; } WebSearchStatus::Error { query } => { writeln!(w, "Web search failed for: {query}")?; } }, AIAgentOutputMessageType::WebFetch(status) => match status { WebFetchStatus::Fetching { urls } => { writeln!(w, "Fetching {} web pages...", urls.len())?; } WebFetchStatus::Success { pages } => { writeln!(w, "Fetched {} web pages", pages.len())?; } WebFetchStatus::Error => { writeln!(w, "Web fetch failed")?; } }, AIAgentOutputMessageType::CommentsAddressed { comments: comment_ids, } => { writeln!(w, "Addressed {} comments", comment_ids.len())?; } AIAgentOutputMessageType::DebugOutput { text } => { writeln!(w, "[DEBUG] {text}")?; } AIAgentOutputMessageType::ArtifactCreated(data) => match data { ArtifactCreatedData::PullRequest { url, branch } => { writeln!(w, "Created PR: {url} (branch: {branch})")?; } ArtifactCreatedData::Screenshot { artifact_uid, .. } => { writeln!(w, "Screenshot captured (artifact: {artifact_uid})")?; } ArtifactCreatedData::File { artifact_uid, filepath, .. } => { writeln!( w, "File artifact uploaded: {filepath} (artifact: {artifact_uid})" )?; } }, AIAgentOutputMessageType::SkillInvoked(invoked_skill) => { writeln!(w, "Skill Read: {}", invoked_skill.name)?; } AIAgentOutputMessageType::MessagesReceivedFromAgents { messages } => { writeln!(w, "Received {} messages", messages.len())?; } AIAgentOutputMessageType::EventsFromAgents { event_ids } => { writeln!(w, "Received {} agent events", event_ids.len())?; } } } // TODO(REMOTE-22): Format citations. Ok(()) } /// Format a list of TODO items. fn format_todos(todos: &[AIAgentTodo], w: &mut W) -> io::Result<()> { for todo in todos { writeln!(w, "* {}", todo.title)?; } Ok(()) } /// Report that the agent conversation has started. This debug ID can be reported to us for troubleshooting. pub fn conversation_started(conversation_id: &str, w: &mut W) -> io::Result<()> { writeln!( w, "New conversation started with debug ID: {conversation_id}\n" ) } /// Report the run ID with a link to the Oz dashboard. pub fn run_started(run_id: &str, w: &mut W) -> io::Result<()> { let run_url = super::run_url(run_id); writeln!(w, "Run ID: {run_id}")?; writeln!(w, "Open in Oz: {run_url}\n") } /// Report that a shared session has been established. pub fn shared_session_established(join_url: &str, w: &mut W) -> io::Result<()> { writeln!(w, "Sharing session at: {join_url}") } /// Format a list of query patterns. fn format_queries, S: fmt::Display>(queries: I) -> String { match queries.into_iter().exactly_one() { Ok(query) => query.to_string(), Err(queries) => format!("[{}]", queries.format(", ")), } } /// Write an artifact_created message for a plan to stdout. We have a separate function for /// this since we report creation on plan WD sync. pub fn plan_artifact_created( document_id: &str, notebook_link: &str, title: &str, w: &mut W, ) -> io::Result<()> { writeln!( w, "Created plan (title: {title}, id: {document_id}, notebook: {notebook_link})" ) } } pub mod json { use std::borrow::Cow; use std::io::{self, Write}; use std::ops::Range; use serde::Serialize; use crate::ai::agent::comment::ReviewComment; use crate::ai::agent::{ AIAgentActionResultType, AIAgentActionType, AIAgentInput, AIAgentOutput, AIAgentOutputMessage, AIAgentOutputMessageType, AIAgentTodo, ArtifactCreatedData, CallMCPToolResult, FileContext, FileGlobResult, FileGlobV2Result, GrepResult, ReadFilesResult, ReadMCPResourceResult, RequestCommandOutputResult, RequestFileEditsResult, SearchCodebaseResult, SubagentCall, TodoOperation, UploadArtifactResult, WriteToLongRunningShellCommandResult, }; use crate::code::buffer_location::LocalOrRemotePath; /// JSON representation of messages in an agent conversation. This is intentionally not 1:1 with our internal `AIAgent*` types - it's /// a stable interface for callers. #[derive(Serialize)] #[serde(tag = "type")] enum JsonMessage<'a> { #[serde(rename = "tool_result")] ToolResult(JsonToolResult<'a>), #[serde(rename = "tool_canceled")] ToolCanceled, #[serde(rename = "tool_error")] ToolError { error: Cow<'a, str>, }, #[serde(rename = "tool_call")] ToolCall(JsonToolCall<'a>), #[serde(rename = "agent")] AgentOutput { text: String, }, #[serde(rename = "agent_reasoning")] AgentReasoning { text: String, }, #[serde(rename = "update_todos")] UpdateTodos { todo_list: Vec>, }, #[serde(rename = "complete_todos")] MarkTodosCompleted { completed_todos: Vec>, }, Subagent { task_id: &'a str, }, #[serde(rename = "system")] System(JsonSystemEvent<'a>), #[serde(rename = "num_comments_addressed")] CommentsAddressed { addressed_comments: Vec>, }, #[serde(rename = "artifact_created")] ArtifactCreated(JsonArtifact<'a>), SkillInvoked { name: &'a str, }, } #[derive(Serialize)] #[serde(tag = "event_type", rename_all = "snake_case")] enum JsonSystemEvent<'a> { ConversationStarted { conversation_id: &'a str }, RunStarted { run_id: &'a str, run_url: &'a str }, SharedSessionEstablished { join_url: &'a str }, } #[derive(Serialize)] #[serde(tag = "tool", rename_all = "snake_case")] enum JsonToolCall<'a> { RunCommand { command: &'a str, }, WriteToCommand, ReadFiles { files: Vec>, }, UploadArtifact { path: &'a str, description: Option<&'a str>, }, SearchCodebase { query: &'a str, codebase: Option<&'a str>, }, EditFiles { title: Option<&'a str>, file_paths: Vec<&'a str>, }, Grep { queries: &'a [String], path: &'a str, }, FileGlob { patterns: &'a [String], path: Option<&'a str>, }, ReadMcpResource { name: &'a str, uri: Option<&'a str>, }, CallMcpTool { name: &'a str, input: &'a serde_json::Value, }, } #[derive(Serialize)] #[serde(tag = "tool", rename_all = "snake_case")] enum JsonToolResult<'a> { RunCommand(JsonRunCommandResult<'a>), EditFiles(JsonEditFilesResult<'a>), ReadFiles(JsonFileCollectionResult<'a>), UploadArtifact(JsonUploadArtifactResult<'a>), SearchCodebase(JsonFileCollectionResult<'a>), Grep(JsonFileCollectionResult<'a>), FileGlob(JsonFileCollectionResult<'a>), ReadMcpResource(JsonReadMcpResourceResult<'a>), CallMcpTool(JsonCallMcpToolResult<'a>), } #[derive(Serialize)] #[serde(tag = "status", rename_all = "snake_case")] enum JsonRunCommandResult<'a> { Complete { exit_code: i32, output: &'a str }, Running, } #[derive(Serialize)] struct JsonEditFilesResult<'a> { diff: &'a str, } #[derive(Serialize)] struct JsonFileCollectionResult<'a> { files: Vec>, } #[derive(Serialize)] struct JsonUploadArtifactResult<'a> { artifact_uid: &'a str, #[serde(skip_serializing_if = "Option::is_none")] filepath: Option<&'a str>, mime_type: &'a str, #[serde(skip_serializing_if = "Option::is_none")] description: Option<&'a str>, size_bytes: i64, } #[derive(Serialize)] struct JsonFile<'a> { path: &'a str, #[serde(skip_serializing_if = "Vec::is_empty")] lines: Vec>, } #[derive(Serialize)] struct JsonReadMcpResourceResult<'a> { resource_contents: &'a [rmcp::model::ResourceContents], } #[derive(Serialize)] struct JsonCallMcpToolResult<'a> { result: &'a rmcp::model::CallToolResult, } #[derive(Serialize)] struct JsonTodo<'a> { title: &'a str, description: &'a str, } #[derive(Serialize)] struct JsonComment<'a> { comment_text: &'a str, file_path: Option, line_number: Option, head_title: Option<&'a str>, } #[derive(Serialize)] #[serde(tag = "artifact_type", rename_all = "snake_case")] enum JsonArtifact<'a> { PullRequest { url: &'a str, branch: &'a str, }, Plan { document_id: &'a str, notebook_link: &'a str, title: &'a str, }, Screenshot { artifact_uid: &'a str, mime_type: &'a str, description: Option<&'a str>, }, File { artifact_uid: &'a str, filepath: &'a str, filename: &'a str, mime_type: &'a str, description: Option<&'a str>, size_bytes: i64, }, } impl<'a> JsonMessage<'a> { fn from_input(input: &'a AIAgentInput) -> Option { match input { // Do not include the user query, since it's already provided as input to the agent. AIAgentInput::UserQuery { .. } | AIAgentInput::CommandCompletionAssessment { .. } | AIAgentInput::AutoCodeDiffQuery { .. } | AIAgentInput::CreateNewProject { .. } | AIAgentInput::CloneRepository { .. } | AIAgentInput::InitProjectRules { .. } | AIAgentInput::CodeReview { .. } | AIAgentInput::FetchReviewComments { .. } | AIAgentInput::CreateEnvironment { .. } | AIAgentInput::SummarizeConversation { .. } | AIAgentInput::InvokeSkill { .. } | AIAgentInput::StartFromAmbientRunPrompt { .. } | AIAgentInput::MessagesReceivedFromAgents { .. } | AIAgentInput::EventsFromAgents { .. } | AIAgentInput::PassiveSuggestionResult { .. } | AIAgentInput::OrchestrationConfigUpdate { .. } => None, // These input types should not occur in a SDK-run agent. AIAgentInput::ResumeConversation { .. } | AIAgentInput::TriggerPassiveSuggestion { .. } => None, AIAgentInput::ActionResult { result, .. } => { Self::from_action_result(&result.result) } } } fn from_action_result(result: &'a AIAgentActionResultType) -> Option { match result { AIAgentActionResultType::RequestCommandOutput(result) => match result { RequestCommandOutputResult::Completed { output, exit_code, .. } => Some(JsonMessage::ToolResult(JsonToolResult::RunCommand( JsonRunCommandResult::Complete { exit_code: exit_code.value(), output, }, ))), RequestCommandOutputResult::LongRunningCommandSnapshot { .. } => { Some(JsonMessage::ToolResult(JsonToolResult::RunCommand( JsonRunCommandResult::Running, ))) } RequestCommandOutputResult::CancelledBeforeExecution => { Some(JsonMessage::ToolCanceled) } RequestCommandOutputResult::ExecutionError { message, .. } => { Some(JsonMessage::ToolError { error: Cow::Borrowed(message), }) } RequestCommandOutputResult::Denylisted { .. } => Some(JsonMessage::ToolError { error: Cow::Borrowed( "Command was not allowed to run due to presence on denylist", ), }), }, AIAgentActionResultType::WriteToLongRunningShellCommand(result) => match result { WriteToLongRunningShellCommandResult::Snapshot { .. } => { Some(JsonMessage::ToolResult(JsonToolResult::RunCommand( JsonRunCommandResult::Running, ))) } WriteToLongRunningShellCommandResult::CommandFinished { output, exit_code, .. } => Some(JsonMessage::ToolResult(JsonToolResult::RunCommand( JsonRunCommandResult::Complete { exit_code: exit_code.value(), output, }, ))), WriteToLongRunningShellCommandResult::Error(_) => { Some(JsonMessage::ToolError { error: "Failed to write to command.".into(), }) } WriteToLongRunningShellCommandResult::Cancelled => { Some(JsonMessage::ToolCanceled) } }, AIAgentActionResultType::RequestFileEdits(result) => match result { RequestFileEditsResult::Success { diff, .. } => Some(JsonMessage::ToolResult( JsonToolResult::EditFiles(JsonEditFilesResult { diff }), )), RequestFileEditsResult::DiffApplicationFailed { error } => { Some(JsonMessage::ToolError { error: Cow::Borrowed(error.as_str()), }) } RequestFileEditsResult::Cancelled => Some(JsonMessage::ToolCanceled), }, AIAgentActionResultType::ReadFiles(result) => match result { ReadFilesResult::Success { files } => Some(JsonMessage::ToolResult( JsonToolResult::ReadFiles(JsonFileCollectionResult { files: JsonFile::from_file_contexts(files), }), )), ReadFilesResult::Error(error) => Some(JsonMessage::ToolError { error: Cow::Borrowed(error.as_str()), }), ReadFilesResult::Cancelled => Some(JsonMessage::ToolCanceled), }, AIAgentActionResultType::UploadArtifact(result) => match result { UploadArtifactResult::Success { artifact_uid, filepath, mime_type, description, size_bytes, } => Some(JsonMessage::ToolResult(JsonToolResult::UploadArtifact( JsonUploadArtifactResult { artifact_uid, filepath: filepath.as_deref(), mime_type, description: description.as_deref(), size_bytes: *size_bytes, }, ))), UploadArtifactResult::Error(error) => Some(JsonMessage::ToolError { error: Cow::Borrowed(error.as_str()), }), UploadArtifactResult::Cancelled => Some(JsonMessage::ToolCanceled), }, AIAgentActionResultType::SearchCodebase(result) => match result { SearchCodebaseResult::Success { files } => Some(JsonMessage::ToolResult( JsonToolResult::SearchCodebase(JsonFileCollectionResult { files: JsonFile::from_file_contexts(files), }), )), SearchCodebaseResult::Failed { message, .. } => Some(JsonMessage::ToolError { error: Cow::Borrowed(message.as_str()), }), SearchCodebaseResult::Cancelled => Some(JsonMessage::ToolCanceled), }, AIAgentActionResultType::Grep(result) => match result { GrepResult::Success { matched_files } => { use crate::ai::agent::GrepFileMatch; let files: Vec = matched_files .iter() .map(|m: &GrepFileMatch| JsonFile { path: m.file_path.as_str(), lines: m .matched_lines .iter() .map(|lm| lm.line_number..(lm.line_number.saturating_add(1))) .collect(), }) .collect(); Some(JsonMessage::ToolResult(JsonToolResult::Grep( JsonFileCollectionResult { files }, ))) } GrepResult::Error(error) => Some(JsonMessage::ToolError { error: Cow::Borrowed(error.as_str()), }), GrepResult::Cancelled => Some(JsonMessage::ToolCanceled), }, AIAgentActionResultType::FileGlobV2(result) => match result { FileGlobV2Result::Success { matched_files, .. } => { let files: Vec = matched_files .iter() .map(|m| JsonFile { path: m.file_path.as_str(), lines: Vec::new(), }) .collect(); Some(JsonMessage::ToolResult(JsonToolResult::FileGlob( JsonFileCollectionResult { files }, ))) } FileGlobV2Result::Error(error) => Some(JsonMessage::ToolError { error: Cow::Borrowed(error.as_str()), }), FileGlobV2Result::Cancelled => Some(JsonMessage::ToolCanceled), }, AIAgentActionResultType::FileGlob(result) => match result { FileGlobResult::Success { matched_files } => { let files: Vec = matched_files .lines() .filter_map(|line| { let p = line.trim(); if p.is_empty() { None } else { Some(JsonFile { path: p, lines: Vec::new(), }) } }) .collect(); Some(JsonMessage::ToolResult(JsonToolResult::FileGlob( JsonFileCollectionResult { files }, ))) } FileGlobResult::Error(error) => Some(JsonMessage::ToolError { error: Cow::Borrowed(error.as_str()), }), FileGlobResult::Cancelled => Some(JsonMessage::ToolCanceled), }, AIAgentActionResultType::ReadMCPResource(result) => match result { ReadMCPResourceResult::Success { resource_contents } => { Some(JsonMessage::ToolResult(JsonToolResult::ReadMcpResource( JsonReadMcpResourceResult { resource_contents }, ))) } ReadMCPResourceResult::Error(error) => Some(JsonMessage::ToolError { error: Cow::Borrowed(error.as_str()), }), ReadMCPResourceResult::Cancelled => Some(JsonMessage::ToolCanceled), }, AIAgentActionResultType::CallMCPTool(result) => match result { CallMCPToolResult::Success { result } => Some(JsonMessage::ToolResult( JsonToolResult::CallMcpTool(JsonCallMcpToolResult { result }), )), CallMCPToolResult::Error(error) => Some(JsonMessage::ToolError { error: Cow::Borrowed(error.as_str()), }), CallMCPToolResult::Cancelled => Some(JsonMessage::ToolCanceled), }, _ => None, } } fn from_output_message(output: &'a AIAgentOutputMessage) -> Option { match &output.message { AIAgentOutputMessageType::Text(text) => { let mut buf = Vec::::new(); super::format_agent_text(text, &mut buf).ok()?; let text = String::from_utf8(buf).ok()?; Some(JsonMessage::AgentOutput { text }) } AIAgentOutputMessageType::Reasoning { text, .. } => { let mut buf = Vec::::new(); super::format_agent_text(text, &mut buf).ok()?; let text = String::from_utf8(buf).ok()?; Some(JsonMessage::AgentReasoning { text }) } AIAgentOutputMessageType::Summarization { text, .. } => { let mut buf = Vec::::new(); super::format_agent_text(text, &mut buf).ok()?; let text = String::from_utf8(buf).ok()?; Some(JsonMessage::AgentReasoning { text }) } AIAgentOutputMessageType::Action(action) => match &action.action { AIAgentActionType::RequestCommandOutput { command, .. } => { Some(JsonMessage::ToolCall(JsonToolCall::RunCommand { command })) } AIAgentActionType::WriteToLongRunningShellCommand { .. } => { Some(JsonMessage::ToolCall(JsonToolCall::WriteToCommand)) } AIAgentActionType::ReadFiles(request) => { let files = request .locations .iter() .map(|loc| JsonFile { path: loc.name.as_str(), lines: loc.lines.clone(), }) .collect(); Some(JsonMessage::ToolCall(JsonToolCall::ReadFiles { files })) } AIAgentActionType::UploadArtifact(request) => { Some(JsonMessage::ToolCall(JsonToolCall::UploadArtifact { path: request.file_path.as_str(), description: request.description.as_deref(), })) } AIAgentActionType::SearchCodebase(request) => { Some(JsonMessage::ToolCall(JsonToolCall::SearchCodebase { query: request.query.as_str(), codebase: request.codebase_path.as_deref(), })) } AIAgentActionType::RequestFileEdits { file_edits, title } => { let file_paths: Vec<&str> = file_edits.iter().filter_map(|edit| edit.file()).collect(); Some(JsonMessage::ToolCall(JsonToolCall::EditFiles { title: title.as_deref(), file_paths, })) } AIAgentActionType::Grep { queries, path } => { Some(JsonMessage::ToolCall(JsonToolCall::Grep { queries, path: path.as_str(), })) } AIAgentActionType::FileGlob { patterns, path } => { Some(JsonMessage::ToolCall(JsonToolCall::FileGlob { patterns, path: path.as_deref(), })) } AIAgentActionType::FileGlobV2 { patterns, search_dir, } => Some(JsonMessage::ToolCall(JsonToolCall::FileGlob { patterns, path: search_dir.as_deref(), })), AIAgentActionType::ReadMCPResource { server_id: _, name, uri, } => Some(JsonMessage::ToolCall(JsonToolCall::ReadMcpResource { name, uri: uri.as_deref(), })), AIAgentActionType::CallMCPTool { server_id: _, name, input, } => Some(JsonMessage::ToolCall(JsonToolCall::CallMcpTool { name, input, })), // TODO(AGENT-2281): implement AIAgentActionType::UseComputer(_use_computer_request) => None, // TODO(AGENT-2281): implement AIAgentActionType::RequestComputerUse(_) => None, // Internal or non-CLI tool calls: skip them AIAgentActionType::SuggestNewConversation { .. } | AIAgentActionType::SuggestPrompt { .. } | AIAgentActionType::InitProject | AIAgentActionType::OpenCodeReview | AIAgentActionType::InsertCodeReviewComments { .. } | AIAgentActionType::ReadDocuments(_) | AIAgentActionType::EditDocuments(_) | AIAgentActionType::CreateDocuments(_) | AIAgentActionType::ReadShellCommandOutput { .. } | AIAgentActionType::ReadSkill(_) | AIAgentActionType::FetchConversation { .. } | AIAgentActionType::StartAgent { .. } | AIAgentActionType::SendMessageToAgent { .. } | AIAgentActionType::TransferShellCommandControlToUser { .. } => None, AIAgentActionType::AskUserQuestion { .. } => None, // RunAgents is desktop-client-only; SDK has no JSON // representation for it. AIAgentActionType::RunAgents(_) => None, AIAgentActionType::WaitForEvents { .. } => None, }, AIAgentOutputMessageType::TodoOperation(operation) => match operation { TodoOperation::UpdateTodos { todos } => Some(JsonMessage::UpdateTodos { todo_list: JsonTodo::from_todos(todos), }), TodoOperation::MarkAsCompleted { completed_todos } => { Some(JsonMessage::MarkTodosCompleted { completed_todos: JsonTodo::from_todos(completed_todos), }) } }, AIAgentOutputMessageType::Subagent(SubagentCall { task_id, .. }) => { Some(JsonMessage::Subagent { task_id }) } AIAgentOutputMessageType::WebSearch(_) => None, AIAgentOutputMessageType::WebFetch(_) => None, AIAgentOutputMessageType::DebugOutput { .. } => None, AIAgentOutputMessageType::CommentsAddressed { comments } => { Some(JsonMessage::CommentsAddressed { addressed_comments: JsonComment::from_review_comments(comments), }) } AIAgentOutputMessageType::ArtifactCreated(data) => { Some(JsonMessage::ArtifactCreated(JsonArtifact::from(data))) } AIAgentOutputMessageType::SkillInvoked(invoked_skill) => { Some(JsonMessage::SkillInvoked { name: &invoked_skill.name, }) } AIAgentOutputMessageType::MessagesReceivedFromAgents { .. } | AIAgentOutputMessageType::EventsFromAgents { .. } | AIAgentOutputMessageType::RuntimeActivity(_) => None, } } } impl<'a> JsonFile<'a> { fn from_file_contexts(contexts: &'a [FileContext]) -> Vec { contexts.iter().map(Self::from).collect() } } impl<'a> From<&'a FileContext> for JsonFile<'a> { fn from(context: &'a FileContext) -> Self { Self { path: context.file_name.as_str(), lines: context.line_range.clone().into_iter().collect(), } } } impl<'a> JsonComment<'a> { fn from_review_comments(comments: &'a [ReviewComment]) -> Vec { comments.iter().map(Self::from).collect() } } impl<'a> From<&'a ReviewComment> for JsonComment<'a> { fn from(review_comment: &'a ReviewComment) -> Self { Self { comment_text: review_comment.content.as_str(), file_path: review_comment .diff .file_path .as_ref() .map(LocalOrRemotePath::display_path), line_number: review_comment.diff.line_number, head_title: review_comment.head_title.as_deref(), } } } impl<'a> JsonTodo<'a> { fn from_todos(todos: &'a [AIAgentTodo]) -> Vec { todos.iter().map(Self::from).collect() } } impl<'a> From<&'a AIAgentTodo> for JsonTodo<'a> { fn from(todo: &'a AIAgentTodo) -> Self { Self { title: todo.title.as_str(), description: todo.description.as_str(), } } } impl<'a> From<&'a ArtifactCreatedData> for JsonArtifact<'a> { fn from(data: &'a ArtifactCreatedData) -> Self { match data { ArtifactCreatedData::PullRequest { url, branch } => JsonArtifact::PullRequest { url: url.as_str(), branch: branch.as_str(), }, ArtifactCreatedData::Screenshot { artifact_uid, mime_type, description, } => JsonArtifact::Screenshot { artifact_uid: artifact_uid.as_str(), mime_type: mime_type.as_str(), description: description.as_deref(), }, ArtifactCreatedData::File { artifact_uid, filepath, filename, mime_type, description, size_bytes, } => JsonArtifact::File { artifact_uid: artifact_uid.as_str(), filepath: filepath.as_str(), filename: filename.as_str(), mime_type: mime_type.as_str(), description: description.as_deref(), size_bytes: *size_bytes, }, } } } /// Write an artifact_created message for a plan to stdout. pub fn plan_artifact_created( document_id: &str, notebook_link: &str, title: &str, w: &mut W, ) -> io::Result<()> { let message = JsonMessage::ArtifactCreated(JsonArtifact::Plan { document_id, notebook_link, title, }); write_message(&message, w) } fn write_message(message: &JsonMessage, w: &mut W) -> io::Result<()> { serde_json::to_writer(&mut *w, message).map_err(|e| io::Error::other(e.to_string()))?; writeln!(w)?; Ok(()) } pub fn format_output(output: &AIAgentOutput, w: &mut W) -> io::Result<()> { for message in output.messages.iter() { if let Some(message) = JsonMessage::from_output_message(message) { write_message(&message, w)?; } } Ok(()) } pub fn format_input(input: &AIAgentInput, w: &mut W) -> io::Result<()> { match JsonMessage::from_input(input) { Some(message) => write_message(&message, w), None => Ok(()), } } /// Write a conversation_started system event to stdout. pub fn conversation_started(conversation_id: &str, w: &mut W) -> io::Result<()> { let message = JsonMessage::System(JsonSystemEvent::ConversationStarted { conversation_id }); write_message(&message, w) } /// Write a run_started system event to stdout. pub fn run_started(run_id: &str, w: &mut W) -> io::Result<()> { let run_url = super::run_url(run_id); let message = JsonMessage::System(JsonSystemEvent::RunStarted { run_id, run_url: &run_url, }); write_message(&message, w) } /// Write a shared_session_established system event to stdout. pub fn shared_session_established(join_url: &str, w: &mut W) -> io::Result<()> { let message = JsonMessage::System(JsonSystemEvent::SharedSessionEstablished { join_url }); write_message(&message, w) } } use std::io::{self, BufWriter, Write}; use galaxy_core::channel::ChannelState; use crate::ai::agent::{AIAgentText, AIAgentTextSection}; use crate::code::editor_management::CodeSource; /// Constructs the Oz dashboard URL for a given run ID. fn run_url(run_id: &str) -> String { let oz_root_url = ChannelState::oz_root_url(); format!("{oz_root_url}/runs/{run_id}") } /// Execute a closure with a buffered stdout writer and flush it afterwards. pub fn with_stdout_buffered(f: F) -> io::Result<()> where F: FnOnce(&mut BufWriter) -> io::Result<()>, { let stdout = io::stdout(); let handle = stdout.lock(); let mut buf = BufWriter::new(handle); f(&mut buf)?; buf.flush() } fn format_agent_text(text: &AIAgentText, w: &mut W) -> io::Result<()> { let mut wrote_newline = false; for section in &text.sections { match section { AIAgentTextSection::PlainText { text } => { write!(w, "{}", text.text())?; wrote_newline = text.text().ends_with('\n'); } AIAgentTextSection::Code { code, language, source, } => { write!(w, "```")?; if let Some(language) = language { write!(w, "{}", language.display_name())?; } match source { Some(CodeSource::ProjectRules { location }) => { writeln!(w, " rules_path={}", location.display_path())?; } Some(CodeSource::Link { path, range_start, range_end, }) => { write!(w, " path={}", path.display())?; if let Some(start) = range_start { write!(w, " start={}", start.line_num)?; } if let Some(end) = range_end { write!(w, " end={}", end.line_num)?; } writeln!(w)?; } Some(CodeSource::Skill { location, .. }) => { writeln!(w, " skill_path={}", location.display_path())?; } Some(CodeSource::AIAction { .. }) | Some(CodeSource::New { .. }) | Some(CodeSource::FileTree { .. }) | Some(CodeSource::CommandPalette { .. }) | Some(CodeSource::Finder { .. }) | None => {} } writeln!(w, "{code}\n```",)?; wrote_newline = true; } AIAgentTextSection::Table { table } => { write!(w, "{}", table.markdown_source)?; wrote_newline = table.markdown_source.ends_with('\n'); } AIAgentTextSection::Image { image } => { write!(w, "{}", image.markdown_source)?; wrote_newline = image.markdown_source.ends_with('\n'); } AIAgentTextSection::MermaidDiagram { diagram } => { write!(w, "{}", diagram.markdown_source)?; wrote_newline = diagram.markdown_source.ends_with('\n'); } } } if !wrote_newline { writeln!(w)?; } Ok(()) }