mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 19:05:18 +00:00
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:
parent
cacb63ef17
commit
e3c030f66d
57 changed files with 927 additions and 771 deletions
|
|
@ -43,7 +43,9 @@ impl CredentialExtractionOptions {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn extract_claude_credentials(options: &CredentialExtractionOptions) -> Option<ProviderCredentials> {
|
||||
pub fn extract_claude_credentials(
|
||||
options: &CredentialExtractionOptions,
|
||||
) -> Option<ProviderCredentials> {
|
||||
let home_dir = options.home_dir.clone().unwrap_or_else(default_home_dir);
|
||||
let include_oauth = options.include_oauth;
|
||||
|
||||
|
|
@ -88,8 +90,7 @@ pub fn extract_claude_credentials(options: &CredentialExtractionOptions) -> Opti
|
|||
};
|
||||
let access = read_string_field(&data, &["claudeAiOauth", "accessToken"]);
|
||||
if let Some(token) = access {
|
||||
if let Some(expires_at) =
|
||||
read_string_field(&data, &["claudeAiOauth", "expiresAt"])
|
||||
if let Some(expires_at) = read_string_field(&data, &["claudeAiOauth", "expiresAt"])
|
||||
{
|
||||
if is_expired_rfc3339(&expires_at) {
|
||||
continue;
|
||||
|
|
@ -108,7 +109,9 @@ pub fn extract_claude_credentials(options: &CredentialExtractionOptions) -> Opti
|
|||
None
|
||||
}
|
||||
|
||||
pub fn extract_codex_credentials(options: &CredentialExtractionOptions) -> Option<ProviderCredentials> {
|
||||
pub fn extract_codex_credentials(
|
||||
options: &CredentialExtractionOptions,
|
||||
) -> Option<ProviderCredentials> {
|
||||
let home_dir = options.home_dir.clone().unwrap_or_else(default_home_dir);
|
||||
let include_oauth = options.include_oauth;
|
||||
let path = home_dir.join(".codex").join("auth.json");
|
||||
|
|
@ -165,18 +168,18 @@ pub fn extract_opencode_credentials(options: &CredentialExtractionOptions) -> Ex
|
|||
None => continue,
|
||||
};
|
||||
|
||||
let auth_type = config
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let auth_type = config.get("type").and_then(Value::as_str).unwrap_or("");
|
||||
|
||||
let credentials = if auth_type == "api" {
|
||||
config.get("key").and_then(Value::as_str).map(|key| ProviderCredentials {
|
||||
api_key: key.to_string(),
|
||||
source: "opencode".to_string(),
|
||||
auth_type: AuthType::ApiKey,
|
||||
provider: provider_name.to_string(),
|
||||
})
|
||||
config
|
||||
.get("key")
|
||||
.and_then(Value::as_str)
|
||||
.map(|key| ProviderCredentials {
|
||||
api_key: key.to_string(),
|
||||
source: "opencode".to_string(),
|
||||
auth_type: AuthType::ApiKey,
|
||||
provider: provider_name.to_string(),
|
||||
})
|
||||
} else if auth_type == "oauth" && include_oauth {
|
||||
let expires = config.get("expires").and_then(Value::as_i64);
|
||||
if let Some(expires) = expires {
|
||||
|
|
@ -214,7 +217,9 @@ pub fn extract_opencode_credentials(options: &CredentialExtractionOptions) -> Ex
|
|||
} else if provider_name == "openai" {
|
||||
result.openai = Some(credentials.clone());
|
||||
} else {
|
||||
result.other.insert(provider_name.to_string(), credentials.clone());
|
||||
result
|
||||
.other
|
||||
.insert(provider_name.to_string(), credentials.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -222,7 +227,9 @@ pub fn extract_opencode_credentials(options: &CredentialExtractionOptions) -> Ex
|
|||
result
|
||||
}
|
||||
|
||||
pub fn extract_amp_credentials(options: &CredentialExtractionOptions) -> Option<ProviderCredentials> {
|
||||
pub fn extract_amp_credentials(
|
||||
options: &CredentialExtractionOptions,
|
||||
) -> Option<ProviderCredentials> {
|
||||
let home_dir = options.home_dir.clone().unwrap_or_else(default_home_dir);
|
||||
let path = home_dir.join(".amp").join("config.json");
|
||||
let data = read_json_file(&path)?;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,7 @@ use std::fmt;
|
|||
use std::fs;
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{
|
||||
Child,
|
||||
ChildStderr,
|
||||
ChildStdin,
|
||||
ChildStdout,
|
||||
Command,
|
||||
ExitStatus,
|
||||
Stdio,
|
||||
};
|
||||
use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use flate2::read::GzDecoder;
|
||||
|
|
@ -124,17 +116,18 @@ impl AgentManager {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn with_platform(
|
||||
install_dir: impl Into<PathBuf>,
|
||||
platform: Platform,
|
||||
) -> Self {
|
||||
pub fn with_platform(install_dir: impl Into<PathBuf>, platform: Platform) -> Self {
|
||||
Self {
|
||||
install_dir: install_dir.into(),
|
||||
platform,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install(&self, agent: AgentId, options: InstallOptions) -> Result<InstallResult, AgentError> {
|
||||
pub fn install(
|
||||
&self,
|
||||
agent: AgentId,
|
||||
options: InstallOptions,
|
||||
) -> Result<InstallResult, AgentError> {
|
||||
let install_path = self.binary_path(agent);
|
||||
if !options.reinstall {
|
||||
if let Ok(existing_path) = self.resolve_binary(agent) {
|
||||
|
|
@ -148,9 +141,15 @@ impl AgentManager {
|
|||
fs::create_dir_all(&self.install_dir)?;
|
||||
|
||||
match agent {
|
||||
AgentId::Claude => install_claude(&install_path, self.platform, options.version.as_deref())?,
|
||||
AgentId::Codex => install_codex(&install_path, self.platform, options.version.as_deref())?,
|
||||
AgentId::Opencode => install_opencode(&install_path, self.platform, options.version.as_deref())?,
|
||||
AgentId::Claude => {
|
||||
install_claude(&install_path, self.platform, options.version.as_deref())?
|
||||
}
|
||||
AgentId::Codex => {
|
||||
install_codex(&install_path, self.platform, options.version.as_deref())?
|
||||
}
|
||||
AgentId::Opencode => {
|
||||
install_opencode(&install_path, self.platform, options.version.as_deref())?
|
||||
}
|
||||
AgentId::Amp => install_amp(&install_path, self.platform, options.version.as_deref())?,
|
||||
AgentId::Mock => {
|
||||
if !install_path.exists() {
|
||||
|
|
@ -256,10 +255,7 @@ impl AgentManager {
|
|||
command.arg("app-server");
|
||||
}
|
||||
AgentId::Opencode => {
|
||||
command
|
||||
.arg("run")
|
||||
.arg("--format")
|
||||
.arg("json");
|
||||
command.arg("run").arg("--format").arg("json");
|
||||
if let Some(model) = options.model.as_deref() {
|
||||
command.arg("-m").arg(model);
|
||||
}
|
||||
|
|
@ -346,10 +342,15 @@ impl AgentManager {
|
|||
|
||||
fn spawn_codex_app_server(&self, options: SpawnOptions) -> Result<SpawnResult, AgentError> {
|
||||
if options.session_id.is_some() {
|
||||
return Err(AgentError::ResumeUnsupported { agent: AgentId::Codex });
|
||||
return Err(AgentError::ResumeUnsupported {
|
||||
agent: AgentId::Codex,
|
||||
});
|
||||
}
|
||||
let mut command = self.build_command(AgentId::Codex, &options)?;
|
||||
command.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
for (key, value) in options.env {
|
||||
command.env(key, value);
|
||||
}
|
||||
|
|
@ -417,11 +418,11 @@ impl AgentManager {
|
|||
Ok(value) => value,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let message: codex_schema::JsonrpcMessage =
|
||||
match serde_json::from_value(value.clone()) {
|
||||
Ok(message) => message,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let message: codex_schema::JsonrpcMessage = match serde_json::from_value(value.clone())
|
||||
{
|
||||
Ok(message) => message,
|
||||
Err(_) => continue,
|
||||
};
|
||||
match message {
|
||||
codex_schema::JsonrpcMessage::Response(response) => {
|
||||
let response_id = response.id.to_string();
|
||||
|
|
@ -443,11 +444,16 @@ impl AgentManager {
|
|||
params.cwd = cwd.clone();
|
||||
send_json_line(
|
||||
&mut stdin,
|
||||
&codex_schema::ClientRequest::ThreadStart { id: request_id, params },
|
||||
&codex_schema::ClientRequest::ThreadStart {
|
||||
id: request_id,
|
||||
params,
|
||||
},
|
||||
)?;
|
||||
thread_start_id = Some(request_id_str);
|
||||
thread_start_sent = true;
|
||||
} else if thread_start_id.as_deref() == Some(&response_id) && thread_id.is_none() {
|
||||
} else if thread_start_id.as_deref() == Some(&response_id)
|
||||
&& thread_id.is_none()
|
||||
{
|
||||
thread_id = codex_thread_id_from_response(&response.result);
|
||||
}
|
||||
events.push(value);
|
||||
|
|
@ -466,8 +472,11 @@ impl AgentManager {
|
|||
) {
|
||||
completed = true;
|
||||
}
|
||||
if let codex_schema::ServerNotification::ItemCompleted(params) = ¬ification {
|
||||
if matches!(params.item, codex_schema::ThreadItem::AgentMessage { .. }) {
|
||||
if let codex_schema::ServerNotification::ItemCompleted(params) =
|
||||
¬ification
|
||||
{
|
||||
if matches!(params.item, codex_schema::ThreadItem::AgentMessage { .. })
|
||||
{
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -809,17 +818,35 @@ fn codex_thread_id_from_notification(
|
|||
codex_schema::ServerNotification::TurnCompleted(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemStarted(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemCompleted(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemAgentMessageDelta(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemReasoningTextDelta(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemReasoningSummaryTextDelta(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemCommandExecutionOutputDelta(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemFileChangeOutputDelta(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemMcpToolCallProgress(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ThreadTokenUsageUpdated(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemAgentMessageDelta(params) => {
|
||||
Some(params.thread_id.clone())
|
||||
}
|
||||
codex_schema::ServerNotification::ItemReasoningTextDelta(params) => {
|
||||
Some(params.thread_id.clone())
|
||||
}
|
||||
codex_schema::ServerNotification::ItemReasoningSummaryTextDelta(params) => {
|
||||
Some(params.thread_id.clone())
|
||||
}
|
||||
codex_schema::ServerNotification::ItemCommandExecutionOutputDelta(params) => {
|
||||
Some(params.thread_id.clone())
|
||||
}
|
||||
codex_schema::ServerNotification::ItemFileChangeOutputDelta(params) => {
|
||||
Some(params.thread_id.clone())
|
||||
}
|
||||
codex_schema::ServerNotification::ItemMcpToolCallProgress(params) => {
|
||||
Some(params.thread_id.clone())
|
||||
}
|
||||
codex_schema::ServerNotification::ThreadTokenUsageUpdated(params) => {
|
||||
Some(params.thread_id.clone())
|
||||
}
|
||||
codex_schema::ServerNotification::TurnDiffUpdated(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::TurnPlanUpdated(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemCommandExecutionTerminalInteraction(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemReasoningSummaryPartAdded(params) => Some(params.thread_id.clone()),
|
||||
codex_schema::ServerNotification::ItemCommandExecutionTerminalInteraction(params) => {
|
||||
Some(params.thread_id.clone())
|
||||
}
|
||||
codex_schema::ServerNotification::ItemReasoningSummaryPartAdded(params) => {
|
||||
Some(params.thread_id.clone())
|
||||
}
|
||||
codex_schema::ServerNotification::ThreadCompacted(params) => Some(params.thread_id.clone()),
|
||||
_ => None,
|
||||
}
|
||||
|
|
@ -859,36 +886,36 @@ fn extract_session_id(agent: AgentId, events: &[Value]) -> Option<String> {
|
|||
return Some(id.to_string());
|
||||
}
|
||||
}
|
||||
AgentId::Codex => {
|
||||
if let Ok(notification) =
|
||||
serde_json::from_value::<codex_schema::ServerNotification>(event.clone())
|
||||
{
|
||||
match notification {
|
||||
codex_schema::ServerNotification::ThreadStarted(params) => {
|
||||
return Some(params.thread.id);
|
||||
AgentId::Codex => {
|
||||
if let Ok(notification) =
|
||||
serde_json::from_value::<codex_schema::ServerNotification>(event.clone())
|
||||
{
|
||||
match notification {
|
||||
codex_schema::ServerNotification::ThreadStarted(params) => {
|
||||
return Some(params.thread.id);
|
||||
}
|
||||
codex_schema::ServerNotification::TurnStarted(params) => {
|
||||
return Some(params.thread_id);
|
||||
}
|
||||
codex_schema::ServerNotification::TurnCompleted(params) => {
|
||||
return Some(params.thread_id);
|
||||
}
|
||||
codex_schema::ServerNotification::ItemStarted(params) => {
|
||||
return Some(params.thread_id);
|
||||
}
|
||||
codex_schema::ServerNotification::ItemCompleted(params) => {
|
||||
return Some(params.thread_id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
codex_schema::ServerNotification::TurnStarted(params) => {
|
||||
return Some(params.thread_id);
|
||||
}
|
||||
codex_schema::ServerNotification::TurnCompleted(params) => {
|
||||
return Some(params.thread_id);
|
||||
}
|
||||
codex_schema::ServerNotification::ItemStarted(params) => {
|
||||
return Some(params.thread_id);
|
||||
}
|
||||
codex_schema::ServerNotification::ItemCompleted(params) => {
|
||||
return Some(params.thread_id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(id) = event.get("thread_id").and_then(Value::as_str) {
|
||||
return Some(id.to_string());
|
||||
}
|
||||
if let Some(id) = event.get("threadId").and_then(Value::as_str) {
|
||||
return Some(id.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(id) = event.get("thread_id").and_then(Value::as_str) {
|
||||
return Some(id.to_string());
|
||||
}
|
||||
if let Some(id) = event.get("threadId").and_then(Value::as_str) {
|
||||
return Some(id.to_string());
|
||||
}
|
||||
}
|
||||
AgentId::Opencode => {
|
||||
if let Some(id) = event.get("session_id").and_then(Value::as_str) {
|
||||
return Some(id.to_string());
|
||||
|
|
@ -902,7 +929,8 @@ fn extract_session_id(agent: AgentId, events: &[Value]) -> Option<String> {
|
|||
if let Some(id) = extract_nested_string(event, &["properties", "sessionID"]) {
|
||||
return Some(id);
|
||||
}
|
||||
if let Some(id) = extract_nested_string(event, &["properties", "part", "sessionID"]) {
|
||||
if let Some(id) = extract_nested_string(event, &["properties", "part", "sessionID"])
|
||||
{
|
||||
return Some(id);
|
||||
}
|
||||
if let Some(id) = extract_nested_string(event, &["session", "id"]) {
|
||||
|
|
@ -925,7 +953,9 @@ fn extract_result_text(agent: AgentId, events: &[Value]) -> Option<String> {
|
|||
if let Some(result) = event.get("result").and_then(Value::as_str) {
|
||||
return Some(result.to_string());
|
||||
}
|
||||
if let Some(text) = extract_nested_string(event, &["message", "content", "0", "text"]) {
|
||||
if let Some(text) =
|
||||
extract_nested_string(event, &["message", "content", "0", "text"])
|
||||
{
|
||||
return Some(text);
|
||||
}
|
||||
}
|
||||
|
|
@ -974,7 +1004,9 @@ fn extract_result_text(agent: AgentId, events: &[Value]) -> Option<String> {
|
|||
if let Some(delta) = extract_nested_string(event, &["properties", "delta"]) {
|
||||
buffer.push_str(&delta);
|
||||
}
|
||||
if let Some(content) = extract_nested_string(event, &["properties", "part", "content"]) {
|
||||
if let Some(content) =
|
||||
extract_nested_string(event, &["properties", "part", "content"])
|
||||
{
|
||||
buffer.push_str(&content);
|
||||
}
|
||||
}
|
||||
|
|
@ -1178,14 +1210,19 @@ fn download_bytes(url: &Url) -> Result<Vec<u8>, AgentError> {
|
|||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn install_claude(path: &Path, platform: Platform, version: Option<&str>) -> Result<(), AgentError> {
|
||||
fn install_claude(
|
||||
path: &Path,
|
||||
platform: Platform,
|
||||
version: Option<&str>,
|
||||
) -> Result<(), AgentError> {
|
||||
let version = match version {
|
||||
Some(version) => version.to_string(),
|
||||
None => {
|
||||
let url = Url::parse(
|
||||
"https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest",
|
||||
)?;
|
||||
let text = String::from_utf8(download_bytes(&url)?).map_err(|err| AgentError::ExtractFailed(err.to_string()))?;
|
||||
let text = String::from_utf8(download_bytes(&url)?)
|
||||
.map_err(|err| AgentError::ExtractFailed(err.to_string()))?;
|
||||
text.trim().to_string()
|
||||
}
|
||||
};
|
||||
|
|
@ -1210,8 +1247,11 @@ fn install_amp(path: &Path, platform: Platform, version: Option<&str>) -> Result
|
|||
let version = match version {
|
||||
Some(version) => version.to_string(),
|
||||
None => {
|
||||
let url = Url::parse("https://storage.googleapis.com/amp-public-assets-prod-0/cli/cli-version.txt")?;
|
||||
let text = String::from_utf8(download_bytes(&url)?).map_err(|err| AgentError::ExtractFailed(err.to_string()))?;
|
||||
let url = Url::parse(
|
||||
"https://storage.googleapis.com/amp-public-assets-prod-0/cli/cli-version.txt",
|
||||
)?;
|
||||
let text = String::from_utf8(download_bytes(&url)?)
|
||||
.map_err(|err| AgentError::ExtractFailed(err.to_string()))?;
|
||||
text.trim().to_string()
|
||||
}
|
||||
};
|
||||
|
|
@ -1261,7 +1301,11 @@ fn install_codex(path: &Path, platform: Platform, version: Option<&str>) -> Resu
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn install_opencode(path: &Path, platform: Platform, version: Option<&str>) -> Result<(), AgentError> {
|
||||
fn install_opencode(
|
||||
path: &Path,
|
||||
platform: Platform,
|
||||
version: Option<&str>,
|
||||
) -> Result<(), AgentError> {
|
||||
match platform {
|
||||
Platform::MacosArm64 => {
|
||||
let url = match version {
|
||||
|
|
@ -1317,7 +1361,8 @@ fn install_opencode(path: &Path, platform: Platform, version: Option<&str>) -> R
|
|||
fn install_zip_binary(path: &Path, url: &Url, binary_name: &str) -> Result<(), AgentError> {
|
||||
let bytes = download_bytes(url)?;
|
||||
let reader = io::Cursor::new(bytes);
|
||||
let mut archive = zip::ZipArchive::new(reader).map_err(|err| AgentError::ExtractFailed(err.to_string()))?;
|
||||
let mut archive =
|
||||
zip::ZipArchive::new(reader).map_err(|err| AgentError::ExtractFailed(err.to_string()))?;
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive
|
||||
|
|
|
|||
|
|
@ -285,10 +285,7 @@ fn handle_health_response(
|
|||
})
|
||||
}
|
||||
|
||||
fn run_blocking_check<F>(
|
||||
provider: &str,
|
||||
check: F,
|
||||
) -> Result<(), TestAgentConfigError>
|
||||
fn run_blocking_check<F>(provider: &str, check: F) -> Result<(), TestAgentConfigError>
|
||||
where
|
||||
F: FnOnce() -> Result<(), TestAgentConfigError> + Send + 'static,
|
||||
{
|
||||
|
|
@ -301,7 +298,12 @@ where
|
|||
}
|
||||
|
||||
fn detect_system_agents() -> Vec<AgentId> {
|
||||
let candidates = [AgentId::Claude, AgentId::Codex, AgentId::Opencode, AgentId::Amp];
|
||||
let candidates = [
|
||||
AgentId::Claude,
|
||||
AgentId::Codex,
|
||||
AgentId::Opencode,
|
||||
AgentId::Amp,
|
||||
];
|
||||
let install_dir = default_install_dir();
|
||||
candidates
|
||||
.into_iter()
|
||||
|
|
|
|||
|
|
@ -123,7 +123,10 @@ pub enum SandboxError {
|
|||
#[error("agent not installed: {agent}")]
|
||||
AgentNotInstalled { agent: String },
|
||||
#[error("install failed: {agent}")]
|
||||
InstallFailed { agent: String, stderr: Option<String> },
|
||||
InstallFailed {
|
||||
agent: String,
|
||||
stderr: Option<String>,
|
||||
},
|
||||
#[error("agent process exited: {agent}")]
|
||||
AgentProcessExited {
|
||||
agent: String,
|
||||
|
|
@ -167,9 +170,7 @@ impl SandboxError {
|
|||
pub fn to_agent_error(&self) -> AgentError {
|
||||
let (agent, session_id, details) = match self {
|
||||
Self::InvalidRequest { .. } => (None, None, None),
|
||||
Self::UnsupportedAgent { agent } => {
|
||||
(Some(agent.clone()), None, None)
|
||||
}
|
||||
Self::UnsupportedAgent { agent } => (Some(agent.clone()), None, None),
|
||||
Self::AgentNotInstalled { agent } => (Some(agent.clone()), None, None),
|
||||
Self::InstallFailed { agent, stderr } => {
|
||||
let mut map = Map::new();
|
||||
|
|
@ -179,7 +180,11 @@ impl SandboxError {
|
|||
(
|
||||
Some(agent.clone()),
|
||||
None,
|
||||
if map.is_empty() { None } else { Some(Value::Object(map)) },
|
||||
if map.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Value::Object(map))
|
||||
},
|
||||
)
|
||||
}
|
||||
Self::AgentProcessExited {
|
||||
|
|
@ -200,7 +205,11 @@ impl SandboxError {
|
|||
(
|
||||
Some(agent.clone()),
|
||||
None,
|
||||
if map.is_empty() { None } else { Some(Value::Object(map)) },
|
||||
if map.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Value::Object(map))
|
||||
},
|
||||
)
|
||||
}
|
||||
Self::TokenInvalid { message } => {
|
||||
|
|
@ -219,20 +228,12 @@ impl SandboxError {
|
|||
});
|
||||
(None, None, details)
|
||||
}
|
||||
Self::SessionNotFound { session_id } => {
|
||||
(None, Some(session_id.clone()), None)
|
||||
}
|
||||
Self::SessionAlreadyExists { session_id } => {
|
||||
(None, Some(session_id.clone()), None)
|
||||
}
|
||||
Self::SessionNotFound { session_id } => (None, Some(session_id.clone()), None),
|
||||
Self::SessionAlreadyExists { session_id } => (None, Some(session_id.clone()), None),
|
||||
Self::ModeNotSupported { agent, mode } => {
|
||||
let mut map = Map::new();
|
||||
map.insert("mode".to_string(), Value::String(mode.clone()));
|
||||
(
|
||||
Some(agent.clone()),
|
||||
None,
|
||||
Some(Value::Object(map)),
|
||||
)
|
||||
(Some(agent.clone()), None, Some(Value::Object(map)))
|
||||
}
|
||||
Self::StreamError { message } => {
|
||||
let mut map = Map::new();
|
||||
|
|
|
|||
|
|
@ -17,10 +17,7 @@ fn main() {
|
|||
let schema_path = schema_dir.join(file);
|
||||
|
||||
// Tell cargo to rerun if schema changes
|
||||
emit_stdout(&format!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
schema_path.display()
|
||||
));
|
||||
emit_stdout(&format!("cargo:rerun-if-changed={}", schema_path.display()));
|
||||
|
||||
if !schema_path.exists() {
|
||||
emit_stdout(&format!(
|
||||
|
|
@ -48,9 +45,10 @@ fn main() {
|
|||
let contents = type_space.to_stream();
|
||||
|
||||
// Format the generated code
|
||||
let formatted = prettyplease::unparse(&syn::parse2(contents.clone()).unwrap_or_else(|e| {
|
||||
panic!("Failed to parse generated code for {}: {}", name, e)
|
||||
}));
|
||||
let formatted = prettyplease::unparse(
|
||||
&syn::parse2(contents.clone())
|
||||
.unwrap_or_else(|e| panic!("Failed to parse generated code for {}: {}", name, e)),
|
||||
);
|
||||
|
||||
let out_path = Path::new(&out_dir).join(format!("{}.rs", name));
|
||||
fs::write(&out_path, formatted)
|
||||
|
|
|
|||
|
|
@ -17,16 +17,14 @@ fn test_claude_bash_input() {
|
|||
|
||||
#[test]
|
||||
fn test_codex_server_notification() {
|
||||
let notification = codex::ServerNotification::ItemCompleted(
|
||||
codex::ItemCompletedNotification {
|
||||
item: codex::ThreadItem::AgentMessage {
|
||||
id: "msg-123".to_string(),
|
||||
text: "Hello from Codex".to_string(),
|
||||
},
|
||||
thread_id: "thread-123".to_string(),
|
||||
turn_id: "turn-456".to_string(),
|
||||
let notification = codex::ServerNotification::ItemCompleted(codex::ItemCompletedNotification {
|
||||
item: codex::ThreadItem::AgentMessage {
|
||||
id: "msg-123".to_string(),
|
||||
text: "Hello from Codex".to_string(),
|
||||
},
|
||||
);
|
||||
thread_id: "thread-123".to_string(),
|
||||
turn_id: "turn-456".to_string(),
|
||||
});
|
||||
|
||||
let json = serde_json::to_string(¬ification).unwrap();
|
||||
assert!(json.contains("item/completed"));
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ fn main() {
|
|||
let out_path = Path::new(&out_dir).join("openapi.json");
|
||||
|
||||
let openapi = ApiDoc::openapi();
|
||||
let json = serde_json::to_string_pretty(&openapi)
|
||||
.expect("Failed to serialize OpenAPI spec");
|
||||
let json = serde_json::to_string_pretty(&openapi).expect("Failed to serialize OpenAPI spec");
|
||||
|
||||
fs::write(&out_path, json).expect("Failed to write OpenAPI spec");
|
||||
emit_stdout(&format!(
|
||||
|
|
|
|||
|
|
@ -47,7 +47,11 @@ fn init_logging() {
|
|||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(tracing_logfmt::builder().layer().with_writer(std::io::stderr))
|
||||
.with(
|
||||
tracing_logfmt::builder()
|
||||
.layer()
|
||||
.with_writer(std::io::stderr),
|
||||
)
|
||||
.init();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use std::io::{BufRead, BufReader};
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use sandbox_agent_error::SandboxError;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use sandbox_agent_universal_agent_schema::StderrOutput;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
const LOG_RETENTION_DAYS: i64 = 7;
|
||||
const LOG_HEAD_LINES: usize = 20;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Sandbox agent core utilities.
|
||||
|
||||
pub mod credentials;
|
||||
mod agent_server_logs;
|
||||
pub mod credentials;
|
||||
pub mod router;
|
||||
pub mod telemetry;
|
||||
pub mod ui;
|
||||
|
|
|
|||
|
|
@ -79,10 +79,6 @@ struct ServerArgs {
|
|||
#[arg(long = "cors-allow-credentials", short = 'C')]
|
||||
cors_allow_credentials: bool,
|
||||
|
||||
/// Disable default CORS for the inspector (https://inspect.sandboxagent.dev)
|
||||
#[arg(long = "no-inspector-cors")]
|
||||
no_inspector_cors: bool,
|
||||
|
||||
#[arg(long = "no-telemetry")]
|
||||
no_telemetry: bool,
|
||||
}
|
||||
|
|
@ -848,19 +844,11 @@ fn available_providers(credentials: &ExtractedCredentials) -> Vec<String> {
|
|||
providers
|
||||
}
|
||||
|
||||
const INSPECTOR_ORIGIN: &str = "https://inspect.sandboxagent.dev";
|
||||
|
||||
fn build_cors_layer(server: &ServerArgs) -> Result<CorsLayer, CliError> {
|
||||
let mut cors = CorsLayer::new();
|
||||
|
||||
// Build origins list: inspector by default + any additional origins
|
||||
// Build origins list from provided origins
|
||||
let mut origins = Vec::new();
|
||||
if !server.no_inspector_cors {
|
||||
let inspector_origin = INSPECTOR_ORIGIN
|
||||
.parse()
|
||||
.map_err(|_| CliError::InvalidCorsOrigin(INSPECTOR_ORIGIN.to_string()))?;
|
||||
origins.push(inspector_origin);
|
||||
}
|
||||
for origin in &server.cors_allow_origin {
|
||||
let value = origin
|
||||
.parse()
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ use sandbox_agent_universal_agent_schema::{
|
|||
ItemDeltaData, ItemEventData, ItemKind, ItemRole, ItemStatus, PermissionEventData,
|
||||
PermissionStatus, QuestionEventData, QuestionStatus, ReasoningVisibility, SessionEndReason,
|
||||
SessionEndedData, SessionStartedData, StderrOutput, TerminatedBy, UniversalEvent,
|
||||
UniversalEventData,
|
||||
UniversalEventType, UniversalItem,
|
||||
UniversalEventData, UniversalEventType, UniversalItem,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -37,6 +36,7 @@ use tokio_stream::wrappers::BroadcastStream;
|
|||
use tower_http::trace::TraceLayer;
|
||||
use utoipa::{Modify, OpenApi, ToSchema};
|
||||
|
||||
use crate::agent_server_logs::AgentServerLogs;
|
||||
use crate::ui;
|
||||
use sandbox_agent_agent_management::agents::{
|
||||
AgentError as ManagerError, AgentId, AgentManager, InstallOptions, SpawnOptions, StreamingSpawn,
|
||||
|
|
@ -44,7 +44,6 @@ use sandbox_agent_agent_management::agents::{
|
|||
use sandbox_agent_agent_management::credentials::{
|
||||
extract_all_credentials, CredentialExtractionOptions, ExtractedCredentials,
|
||||
};
|
||||
use crate::agent_server_logs::AgentServerLogs;
|
||||
|
||||
const MOCK_EVENT_DELAY_MS: u64 = 200;
|
||||
static USER_MESSAGE_COUNTER: AtomicU64 = AtomicU64::new(1);
|
||||
|
|
@ -99,7 +98,10 @@ pub fn build_router_with_state(shared: Arc<AppState>) -> (Router, Arc<AppState>)
|
|||
.route("/sessions", get(list_sessions))
|
||||
.route("/sessions/:session_id", post(create_session))
|
||||
.route("/sessions/:session_id/messages", post(post_message))
|
||||
.route("/sessions/:session_id/messages/stream", post(post_message_stream))
|
||||
.route(
|
||||
"/sessions/:session_id/messages/stream",
|
||||
post(post_message_stream),
|
||||
)
|
||||
.route("/sessions/:session_id/terminate", post(terminate_session))
|
||||
.route("/sessions/:session_id/events", get(get_events))
|
||||
.route("/sessions/:session_id/events/sse", get(get_events_sse))
|
||||
|
|
@ -126,7 +128,8 @@ pub fn build_router_with_state(shared: Arc<AppState>) -> (Router, Arc<AppState>)
|
|||
|
||||
let mut router = Router::new()
|
||||
.route("/", get(get_root))
|
||||
.nest("/v1", v1_router);
|
||||
.nest("/v1", v1_router)
|
||||
.fallback(not_found);
|
||||
|
||||
if ui::is_enabled() {
|
||||
router = router.merge(ui::router());
|
||||
|
|
@ -1141,8 +1144,8 @@ impl AgentServerManager {
|
|||
) -> Result<(String, Arc<std::sync::Mutex<Option<std::process::Child>>>), SandboxError> {
|
||||
let manager = self.agent_manager.clone();
|
||||
let log_dir = self.log_base_dir.clone();
|
||||
let (base_url, child) =
|
||||
tokio::task::spawn_blocking(move || -> Result<(String, std::process::Child), SandboxError> {
|
||||
let (base_url, child) = tokio::task::spawn_blocking(
|
||||
move || -> Result<(String, std::process::Child), SandboxError> {
|
||||
let path = manager
|
||||
.resolve_binary(agent)
|
||||
.map_err(|err| map_spawn_error(agent, err))?;
|
||||
|
|
@ -1159,16 +1162,14 @@ impl AgentServerManager {
|
|||
message: err.to_string(),
|
||||
})?;
|
||||
Ok((format!("http://127.0.0.1:{port}"), child))
|
||||
})
|
||||
.await
|
||||
.map_err(|err| SandboxError::StreamError {
|
||||
message: err.to_string(),
|
||||
})??;
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|err| SandboxError::StreamError {
|
||||
message: err.to_string(),
|
||||
})??;
|
||||
|
||||
Ok((
|
||||
base_url,
|
||||
Arc::new(std::sync::Mutex::new(Some(child))),
|
||||
))
|
||||
Ok((base_url, Arc::new(std::sync::Mutex::new(Some(child)))))
|
||||
}
|
||||
|
||||
async fn spawn_stdio_server(
|
||||
|
|
@ -1187,59 +1188,66 @@ impl AgentServerManager {
|
|||
let (stdin_tx, stdin_rx) = mpsc::unbounded_channel::<String>();
|
||||
let (stdout_tx, stdout_rx) = mpsc::unbounded_channel::<String>();
|
||||
|
||||
let child = tokio::task::spawn_blocking(move || -> Result<std::process::Child, SandboxError> {
|
||||
let path = manager
|
||||
.resolve_binary(agent)
|
||||
.map_err(|err| map_spawn_error(agent, err))?;
|
||||
let mut command = std::process::Command::new(path);
|
||||
let stderr = AgentServerLogs::new(log_dir, agent.as_str()).open()?;
|
||||
command
|
||||
.arg("app-server")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(stderr);
|
||||
let child =
|
||||
tokio::task::spawn_blocking(move || -> Result<std::process::Child, SandboxError> {
|
||||
let path = manager
|
||||
.resolve_binary(agent)
|
||||
.map_err(|err| map_spawn_error(agent, err))?;
|
||||
let mut command = std::process::Command::new(path);
|
||||
let stderr = AgentServerLogs::new(log_dir, agent.as_str()).open()?;
|
||||
command
|
||||
.arg("app-server")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(stderr);
|
||||
|
||||
let mut child = command.spawn().map_err(|err| SandboxError::StreamError {
|
||||
let mut child = command.spawn().map_err(|err| SandboxError::StreamError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
|
||||
let stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| SandboxError::StreamError {
|
||||
message: "codex stdin unavailable".to_string(),
|
||||
})?;
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| SandboxError::StreamError {
|
||||
message: "codex stdout unavailable".to_string(),
|
||||
})?;
|
||||
|
||||
let stdin_rx_mut = std::sync::Mutex::new(stdin_rx);
|
||||
std::thread::spawn(move || {
|
||||
let mut stdin = stdin;
|
||||
let mut rx = stdin_rx_mut.lock().unwrap();
|
||||
while let Some(line) = rx.blocking_recv() {
|
||||
if writeln!(stdin, "{line}").is_err() {
|
||||
break;
|
||||
}
|
||||
if stdin.flush().is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let reader = BufReader::new(stdout);
|
||||
for line in reader.lines() {
|
||||
let Ok(line) = line else { break };
|
||||
if stdout_tx.send(line).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(child)
|
||||
})
|
||||
.await
|
||||
.map_err(|err| SandboxError::StreamError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
|
||||
let stdin = child.stdin.take().ok_or_else(|| SandboxError::StreamError {
|
||||
message: "codex stdin unavailable".to_string(),
|
||||
})?;
|
||||
let stdout = child.stdout.take().ok_or_else(|| SandboxError::StreamError {
|
||||
message: "codex stdout unavailable".to_string(),
|
||||
})?;
|
||||
|
||||
let stdin_rx_mut = std::sync::Mutex::new(stdin_rx);
|
||||
std::thread::spawn(move || {
|
||||
let mut stdin = stdin;
|
||||
let mut rx = stdin_rx_mut.lock().unwrap();
|
||||
while let Some(line) = rx.blocking_recv() {
|
||||
if writeln!(stdin, "{line}").is_err() {
|
||||
break;
|
||||
}
|
||||
if stdin.flush().is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let reader = BufReader::new(stdout);
|
||||
for line in reader.lines() {
|
||||
let Ok(line) = line else { break };
|
||||
if stdout_tx.send(line).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(child)
|
||||
})
|
||||
.await
|
||||
.map_err(|err| SandboxError::StreamError {
|
||||
message: err.to_string(),
|
||||
})??;
|
||||
})??;
|
||||
|
||||
let server = Arc::new(CodexServer::new(stdin_tx));
|
||||
|
||||
|
|
@ -1347,7 +1355,10 @@ impl AgentServerManager {
|
|||
}
|
||||
}
|
||||
|
||||
async fn ensure_server_for_restart(self: Arc<Self>, agent: AgentId) -> Result<(), SandboxError> {
|
||||
async fn ensure_server_for_restart(
|
||||
self: Arc<Self>,
|
||||
agent: AgentId,
|
||||
) -> Result<(), SandboxError> {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
match agent {
|
||||
AgentId::Opencode => {
|
||||
|
|
@ -1445,26 +1456,24 @@ impl SessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
fn session_ref<'a>(
|
||||
sessions: &'a [SessionState],
|
||||
session_id: &str,
|
||||
) -> Option<&'a SessionState> {
|
||||
sessions.iter().find(|session| session.session_id == session_id)
|
||||
fn session_ref<'a>(sessions: &'a [SessionState], session_id: &str) -> Option<&'a SessionState> {
|
||||
sessions
|
||||
.iter()
|
||||
.find(|session| session.session_id == session_id)
|
||||
}
|
||||
|
||||
fn session_mut<'a>(
|
||||
sessions: &'a mut [SessionState],
|
||||
session_id: &str,
|
||||
) -> Option<&'a mut SessionState> {
|
||||
sessions.iter_mut().find(|session| session.session_id == session_id)
|
||||
sessions
|
||||
.iter_mut()
|
||||
.find(|session| session.session_id == session_id)
|
||||
}
|
||||
|
||||
/// Read agent stderr for error diagnostics
|
||||
fn read_agent_stderr(&self, agent: AgentId) -> Option<StderrOutput> {
|
||||
let logs = AgentServerLogs::new(
|
||||
self.server_manager.log_base_dir.clone(),
|
||||
agent.as_str(),
|
||||
);
|
||||
let logs = AgentServerLogs::new(self.server_manager.log_base_dir.clone(), agent.as_str());
|
||||
logs.read_stderr()
|
||||
}
|
||||
|
||||
|
|
@ -1476,7 +1485,10 @@ impl SessionManager {
|
|||
let agent_id = parse_agent_id(&request.agent)?;
|
||||
{
|
||||
let sessions = self.sessions.lock().await;
|
||||
if sessions.iter().any(|session| session.session_id == session_id) {
|
||||
if sessions
|
||||
.iter()
|
||||
.any(|session| session.session_id == session_id)
|
||||
{
|
||||
return Err(SandboxError::SessionAlreadyExists { session_id });
|
||||
}
|
||||
}
|
||||
|
|
@ -1675,10 +1687,7 @@ impl SessionManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn emit_synthetic_assistant_start(
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> Result<(), SandboxError> {
|
||||
async fn emit_synthetic_assistant_start(&self, session_id: &str) -> Result<(), SandboxError> {
|
||||
let conversion = {
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
let session = Self::session_mut(&mut sessions, session_id).ok_or_else(|| {
|
||||
|
|
@ -1688,7 +1697,9 @@ impl SessionManager {
|
|||
})?;
|
||||
session.enqueue_pending_assistant_start()
|
||||
};
|
||||
let _ = self.record_conversions(session_id, vec![conversion]).await?;
|
||||
let _ = self
|
||||
.record_conversions(session_id, vec![conversion])
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -1905,12 +1916,16 @@ impl SessionManager {
|
|||
let sender = claude_sender.ok_or_else(|| SandboxError::InvalidRequest {
|
||||
message: "Claude session is not active".to_string(),
|
||||
})?;
|
||||
let session_id = native_session_id.clone().unwrap_or_else(|| session_id.to_string());
|
||||
let session_id = native_session_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| session_id.to_string());
|
||||
let response_text = response.clone().unwrap_or_default();
|
||||
let line = claude_tool_result_line(&session_id, question_id, &response_text, false);
|
||||
sender.send(line).map_err(|_| SandboxError::InvalidRequest {
|
||||
message: "Claude session is not active".to_string(),
|
||||
})?;
|
||||
sender
|
||||
.send(line)
|
||||
.map_err(|_| SandboxError::InvalidRequest {
|
||||
message: "Claude session is not active".to_string(),
|
||||
})?;
|
||||
} else {
|
||||
// TODO: Forward question replies to subprocess agents.
|
||||
}
|
||||
|
|
@ -1976,16 +1991,20 @@ impl SessionManager {
|
|||
let sender = claude_sender.ok_or_else(|| SandboxError::InvalidRequest {
|
||||
message: "Claude session is not active".to_string(),
|
||||
})?;
|
||||
let session_id = native_session_id.clone().unwrap_or_else(|| session_id.to_string());
|
||||
let session_id = native_session_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| session_id.to_string());
|
||||
let line = claude_tool_result_line(
|
||||
&session_id,
|
||||
question_id,
|
||||
"User rejected the question.",
|
||||
true,
|
||||
);
|
||||
sender.send(line).map_err(|_| SandboxError::InvalidRequest {
|
||||
message: "Claude session is not active".to_string(),
|
||||
})?;
|
||||
sender
|
||||
.send(line)
|
||||
.map_err(|_| SandboxError::InvalidRequest {
|
||||
message: "Claude session is not active".to_string(),
|
||||
})?;
|
||||
} else {
|
||||
// TODO: Forward question rejections to subprocess agents.
|
||||
}
|
||||
|
|
@ -2139,9 +2158,11 @@ impl SessionManager {
|
|||
};
|
||||
|
||||
let line = claude_control_response_line(permission_id, behavior, response_value);
|
||||
sender.send(line).map_err(|_| SandboxError::InvalidRequest {
|
||||
message: "Claude session is not active".to_string(),
|
||||
})?;
|
||||
sender
|
||||
.send(line)
|
||||
.map_err(|_| SandboxError::InvalidRequest {
|
||||
message: "Claude session is not active".to_string(),
|
||||
})?;
|
||||
} else {
|
||||
// TODO: Forward permission replies to subprocess agents.
|
||||
}
|
||||
|
|
@ -2811,7 +2832,8 @@ impl SessionManager {
|
|||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let message: codex_schema::JsonrpcMessage = match serde_json::from_value(value.clone()) {
|
||||
let message: codex_schema::JsonrpcMessage = match serde_json::from_value(value.clone())
|
||||
{
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
|
@ -2828,12 +2850,17 @@ impl SessionManager {
|
|||
if let Ok(notification) =
|
||||
serde_json::from_value::<codex_schema::ServerNotification>(value.clone())
|
||||
{
|
||||
if let Some(thread_id) = codex_thread_id_from_server_notification(¬ification) {
|
||||
if let Some(thread_id) =
|
||||
codex_thread_id_from_server_notification(¬ification)
|
||||
{
|
||||
if let Some(session_id) = server.session_for_thread(&thread_id) {
|
||||
let conversions = match convert_codex::notification_to_universal(¬ification) {
|
||||
Ok(c) => c,
|
||||
Err(err) => vec![agent_unparsed("codex", &err, value.clone())],
|
||||
};
|
||||
let conversions =
|
||||
match convert_codex::notification_to_universal(¬ification) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
vec![agent_unparsed("codex", &err, value.clone())]
|
||||
}
|
||||
};
|
||||
let _ = self.record_conversions(&session_id, conversions).await;
|
||||
}
|
||||
}
|
||||
|
|
@ -2851,7 +2878,8 @@ impl SessionManager {
|
|||
for conversion in &mut conversions {
|
||||
conversion.raw = Some(value.clone());
|
||||
}
|
||||
let _ = self.record_conversions(&session_id, conversions).await;
|
||||
let _ =
|
||||
self.record_conversions(&session_id, conversions).await;
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = self
|
||||
|
|
@ -2982,12 +3010,13 @@ impl SessionManager {
|
|||
) -> Result<(), SandboxError> {
|
||||
let server = self.ensure_codex_server().await?;
|
||||
|
||||
let thread_id = session
|
||||
.native_session_id
|
||||
.as_ref()
|
||||
.ok_or_else(|| SandboxError::InvalidRequest {
|
||||
message: "missing Codex thread id".to_string(),
|
||||
})?;
|
||||
let thread_id =
|
||||
session
|
||||
.native_session_id
|
||||
.as_ref()
|
||||
.ok_or_else(|| SandboxError::InvalidRequest {
|
||||
message: "missing Codex thread id".to_string(),
|
||||
})?;
|
||||
|
||||
let id = server.next_request_id();
|
||||
let prompt_text = codex_prompt_for_mode(prompt, Some(&session.agent_mode));
|
||||
|
|
@ -3430,14 +3459,22 @@ pub struct EventsQuery {
|
|||
pub offset: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", alias = "include_raw")]
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
alias = "include_raw"
|
||||
)]
|
||||
pub include_raw: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TurnStreamQuery {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", alias = "include_raw")]
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
alias = "include_raw"
|
||||
)]
|
||||
pub include_raw: Option<bool>,
|
||||
}
|
||||
|
||||
|
|
@ -3541,10 +3578,22 @@ async fn get_agent_modes(
|
|||
Ok(Json(AgentModesResponse { modes }))
|
||||
}
|
||||
|
||||
const SERVER_INFO: &str = "\
|
||||
This is a Sandbox Agent server. Available endpoints:\n\
|
||||
- GET / - Server info\n\
|
||||
- GET /v1/health - Health check\n\
|
||||
- GET /ui/ - Inspector UI\n\n\
|
||||
See https://sandboxagent.dev for API documentation.";
|
||||
|
||||
async fn get_root() -> &'static str {
|
||||
"This is a Sandbox Agent server for orchestrating coding agents.\n\
|
||||
See https://sandboxagent.dev for more information.\n\
|
||||
Visit /ui/ for the inspector UI."
|
||||
SERVER_INFO
|
||||
}
|
||||
|
||||
async fn not_found() -> (StatusCode, String) {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("404 Not Found\n\n{SERVER_INFO}"),
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
|
|
@ -3571,48 +3620,47 @@ async fn list_agents(
|
|||
let manager = state.agent_manager.clone();
|
||||
let server_statuses = state.session_manager.server_manager.status_snapshot().await;
|
||||
|
||||
let agents = tokio::task::spawn_blocking(move || {
|
||||
all_agents()
|
||||
.into_iter()
|
||||
.map(|agent_id| {
|
||||
let installed = manager.is_installed(agent_id);
|
||||
let version = manager.version(agent_id).ok().flatten();
|
||||
let path = manager.resolve_binary(agent_id).ok();
|
||||
let capabilities = agent_capabilities_for(agent_id);
|
||||
let agents =
|
||||
tokio::task::spawn_blocking(move || {
|
||||
all_agents()
|
||||
.into_iter()
|
||||
.map(|agent_id| {
|
||||
let installed = manager.is_installed(agent_id);
|
||||
let version = manager.version(agent_id).ok().flatten();
|
||||
let path = manager.resolve_binary(agent_id).ok();
|
||||
let capabilities = agent_capabilities_for(agent_id);
|
||||
|
||||
// Add server_status for agents with shared processes
|
||||
let server_status = if capabilities.shared_process {
|
||||
Some(
|
||||
server_statuses
|
||||
.get(&agent_id)
|
||||
.cloned()
|
||||
.unwrap_or(ServerStatusInfo {
|
||||
status: ServerStatus::Stopped,
|
||||
base_url: None,
|
||||
uptime_ms: None,
|
||||
restart_count: 0,
|
||||
last_error: None,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Add server_status for agents with shared processes
|
||||
let server_status =
|
||||
if capabilities.shared_process {
|
||||
Some(server_statuses.get(&agent_id).cloned().unwrap_or(
|
||||
ServerStatusInfo {
|
||||
status: ServerStatus::Stopped,
|
||||
base_url: None,
|
||||
uptime_ms: None,
|
||||
restart_count: 0,
|
||||
last_error: None,
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
AgentInfo {
|
||||
id: agent_id.as_str().to_string(),
|
||||
installed,
|
||||
version,
|
||||
path: path.map(|path| path.to_string_lossy().to_string()),
|
||||
capabilities,
|
||||
server_status,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await
|
||||
.map_err(|err| SandboxError::StreamError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
AgentInfo {
|
||||
id: agent_id.as_str().to_string(),
|
||||
installed,
|
||||
version,
|
||||
path: path.map(|path| path.to_string_lossy().to_string()),
|
||||
capabilities,
|
||||
server_status,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await
|
||||
.map_err(|err| SandboxError::StreamError {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Json(AgentListResponse { agents }))
|
||||
}
|
||||
|
|
@ -3898,7 +3946,10 @@ fn all_agents() -> [AgentId; 5] {
|
|||
/// Returns true if the agent supports resuming a session after its process exits.
|
||||
/// These agents can use --resume/--continue to continue a conversation.
|
||||
fn agent_supports_resume(agent: AgentId) -> bool {
|
||||
matches!(agent, AgentId::Claude | AgentId::Amp | AgentId::Opencode | AgentId::Codex)
|
||||
matches!(
|
||||
agent,
|
||||
AgentId::Claude | AgentId::Amp | AgentId::Opencode | AgentId::Codex
|
||||
)
|
||||
}
|
||||
|
||||
fn agent_supports_item_started(agent: AgentId) -> bool {
|
||||
|
|
@ -4066,16 +4117,18 @@ fn agent_modes_for(agent: AgentId) -> Vec<AgentModeInfo> {
|
|||
name: "Build".to_string(),
|
||||
description: "Default build mode".to_string(),
|
||||
}],
|
||||
AgentId::Mock => vec![AgentModeInfo {
|
||||
id: "build".to_string(),
|
||||
name: "Build".to_string(),
|
||||
description: "Mock agent for UI testing".to_string(),
|
||||
},
|
||||
AgentModeInfo {
|
||||
id: "plan".to_string(),
|
||||
name: "Plan".to_string(),
|
||||
description: "Plan-only mock mode".to_string(),
|
||||
}],
|
||||
AgentId::Mock => vec![
|
||||
AgentModeInfo {
|
||||
id: "build".to_string(),
|
||||
name: "Build".to_string(),
|
||||
description: "Mock agent for UI testing".to_string(),
|
||||
},
|
||||
AgentModeInfo {
|
||||
id: "plan".to_string(),
|
||||
name: "Plan".to_string(),
|
||||
description: "Plan-only mock mode".to_string(),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4307,16 +4360,9 @@ fn claude_tool_result_line(
|
|||
.to_string()
|
||||
}
|
||||
|
||||
fn claude_control_response_line(
|
||||
request_id: &str,
|
||||
behavior: &str,
|
||||
response: Value,
|
||||
) -> String {
|
||||
fn claude_control_response_line(request_id: &str, behavior: &str, response: Value) -> String {
|
||||
let mut response_obj = serde_json::Map::new();
|
||||
response_obj.insert(
|
||||
"behavior".to_string(),
|
||||
Value::String(behavior.to_string()),
|
||||
);
|
||||
response_obj.insert("behavior".to_string(), Value::String(behavior.to_string()));
|
||||
if let Some(message) = response.get("message") {
|
||||
response_obj.insert("message".to_string(), message.clone());
|
||||
}
|
||||
|
|
@ -5022,7 +5068,8 @@ pub mod test_utils {
|
|||
impl TestHarness {
|
||||
pub async fn new() -> Self {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let agent_manager = Arc::new(AgentManager::new(temp_dir.path()).expect("agent manager"));
|
||||
let agent_manager =
|
||||
Arc::new(AgentManager::new(temp_dir.path()).expect("agent manager"));
|
||||
let session_manager = Arc::new(SessionManager::new(agent_manager));
|
||||
session_manager
|
||||
.server_manager
|
||||
|
|
@ -5058,11 +5105,7 @@ pub mod test_utils {
|
|||
.await;
|
||||
}
|
||||
|
||||
pub async fn has_session_mapping(
|
||||
&self,
|
||||
agent: AgentId,
|
||||
session_id: &str,
|
||||
) -> bool {
|
||||
pub async fn has_session_mapping(&self, agent: AgentId, session_id: &str) -> bool {
|
||||
let sessions = self.session_manager.server_manager.sessions.lock().await;
|
||||
sessions
|
||||
.get(&agent)
|
||||
|
|
@ -5101,8 +5144,8 @@ pub mod test_utils {
|
|||
variant: None,
|
||||
agent_version: None,
|
||||
};
|
||||
let mut session = SessionState::new(session_id.to_string(), agent, &request)
|
||||
.expect("session");
|
||||
let mut session =
|
||||
SessionState::new(session_id.to_string(), agent, &request).expect("session");
|
||||
session.native_session_id = native_session_id.map(|id| id.to_string());
|
||||
self.session_manager.sessions.lock().await.push(session);
|
||||
}
|
||||
|
|
@ -5116,38 +5159,48 @@ pub mod test_utils {
|
|||
let (stdin_tx, _stdin_rx) = mpsc::unbounded_channel::<String>();
|
||||
let server = Arc::new(CodexServer::new(stdin_tx));
|
||||
let child = Arc::new(std::sync::Mutex::new(child));
|
||||
self.session_manager.server_manager.servers.lock().await.insert(
|
||||
agent,
|
||||
ManagedServer {
|
||||
kind: ManagedServerKind::Stdio { server },
|
||||
child: child.clone(),
|
||||
status: ServerStatus::Running,
|
||||
start_time: Some(Instant::now()),
|
||||
restart_count: 0,
|
||||
last_error: None,
|
||||
shutdown_requested: false,
|
||||
instance_id,
|
||||
},
|
||||
);
|
||||
self.session_manager
|
||||
.server_manager
|
||||
.servers
|
||||
.lock()
|
||||
.await
|
||||
.insert(
|
||||
agent,
|
||||
ManagedServer {
|
||||
kind: ManagedServerKind::Stdio { server },
|
||||
child: child.clone(),
|
||||
status: ServerStatus::Running,
|
||||
start_time: Some(Instant::now()),
|
||||
restart_count: 0,
|
||||
last_error: None,
|
||||
shutdown_requested: false,
|
||||
instance_id,
|
||||
},
|
||||
);
|
||||
child
|
||||
}
|
||||
|
||||
pub async fn insert_http_server(&self, agent: AgentId, instance_id: u64) {
|
||||
self.session_manager.server_manager.servers.lock().await.insert(
|
||||
agent,
|
||||
ManagedServer {
|
||||
kind: ManagedServerKind::Http {
|
||||
base_url: "http://127.0.0.1:1".to_string(),
|
||||
self.session_manager
|
||||
.server_manager
|
||||
.servers
|
||||
.lock()
|
||||
.await
|
||||
.insert(
|
||||
agent,
|
||||
ManagedServer {
|
||||
kind: ManagedServerKind::Http {
|
||||
base_url: "http://127.0.0.1:1".to_string(),
|
||||
},
|
||||
child: Arc::new(std::sync::Mutex::new(None)),
|
||||
status: ServerStatus::Running,
|
||||
start_time: Some(Instant::now()),
|
||||
restart_count: 0,
|
||||
last_error: None,
|
||||
shutdown_requested: false,
|
||||
instance_id,
|
||||
},
|
||||
child: Arc::new(std::sync::Mutex::new(None)),
|
||||
status: ServerStatus::Running,
|
||||
start_time: Some(Instant::now()),
|
||||
restart_count: 0,
|
||||
last_error: None,
|
||||
shutdown_requested: false,
|
||||
instance_id,
|
||||
},
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn handle_process_exit(
|
||||
|
|
@ -5236,7 +5289,12 @@ pub mod test_utils {
|
|||
fn default_log_dir() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.map(|dir| dir.join("sandbox-agent").join("logs").join("servers"))
|
||||
.unwrap_or_else(|| PathBuf::from(".").join(".sandbox-agent").join("logs").join("servers"))
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(".")
|
||||
.join(".sandbox-agent")
|
||||
.join("logs")
|
||||
.join("servers")
|
||||
})
|
||||
}
|
||||
|
||||
fn find_available_port() -> Result<u16, SandboxError> {
|
||||
|
|
@ -5287,7 +5345,6 @@ impl SseAccumulator {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fn parse_opencode_modes(value: &Value) -> Vec<AgentModeInfo> {
|
||||
let mut modes = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
|
|
|||
|
|
@ -90,7 +90,8 @@ pub fn spawn_telemetry_task() {
|
|||
|
||||
attempt_send(&client).await;
|
||||
let start = Instant::now() + Duration::from_secs(TELEMETRY_INTERVAL_SECS);
|
||||
let mut interval = tokio::time::interval_at(start, Duration::from_secs(TELEMETRY_INTERVAL_SECS));
|
||||
let mut interval =
|
||||
tokio::time::interval_at(start, Duration::from_secs(TELEMETRY_INTERVAL_SECS));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
attempt_send(&client).await;
|
||||
|
|
@ -150,7 +151,12 @@ fn load_or_create_id() -> String {
|
|||
}
|
||||
}
|
||||
|
||||
if let Ok(mut file) = fs::OpenOptions::new().create(true).write(true).truncate(true).open(&path) {
|
||||
if let Ok(mut file) = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&path)
|
||||
{
|
||||
let _ = file.write_all(id.as_bytes());
|
||||
}
|
||||
id
|
||||
|
|
@ -194,7 +200,12 @@ fn write_last_sent(timestamp: i64) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
if let Ok(mut file) = fs::OpenOptions::new().create(true).write(true).truncate(true).open(&path) {
|
||||
if let Ok(mut file) = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&path)
|
||||
{
|
||||
let _ = file.write_all(timestamp.to_string().as_bytes());
|
||||
}
|
||||
}
|
||||
|
|
@ -266,7 +277,8 @@ fn detect_provider() -> ProviderInfo {
|
|||
};
|
||||
}
|
||||
|
||||
if env::var("MODAL_IS_REMOTE").as_deref() == Ok("1") || env::var("MODAL_CLOUD_PROVIDER").is_ok() {
|
||||
if env::var("MODAL_IS_REMOTE").as_deref() == Ok("1") || env::var("MODAL_CLOUD_PROVIDER").is_ok()
|
||||
{
|
||||
let metadata = metadata_or_none([
|
||||
("cloudProvider", env::var("MODAL_CLOUD_PROVIDER").ok()),
|
||||
("region", env::var("MODAL_REGION").ok()),
|
||||
|
|
@ -395,7 +407,9 @@ fn detect_docker() -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
fn filter_metadata(pairs: impl IntoIterator<Item = (&'static str, Option<String>)>) -> HashMap<String, String> {
|
||||
fn filter_metadata(
|
||||
pairs: impl IntoIterator<Item = (&'static str, Option<String>)>,
|
||||
) -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
for (key, value) in pairs {
|
||||
if let Some(value) = value {
|
||||
|
|
@ -407,7 +421,9 @@ fn filter_metadata(pairs: impl IntoIterator<Item = (&'static str, Option<String>
|
|||
map
|
||||
}
|
||||
|
||||
fn metadata_or_none(pairs: impl IntoIterator<Item = (&'static str, Option<String>)>) -> Option<HashMap<String, String>> {
|
||||
fn metadata_or_none(
|
||||
pairs: impl IntoIterator<Item = (&'static str, Option<String>)>,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
let map = filter_metadata(pairs);
|
||||
if map.is_empty() {
|
||||
None
|
||||
|
|
|
|||
|
|
@ -37,7 +37,11 @@ fn serve_path(path: &str) -> Response {
|
|||
};
|
||||
|
||||
let trimmed = path.trim_start_matches('/');
|
||||
let target = if trimmed.is_empty() { "index.html" } else { trimmed };
|
||||
let target = if trimmed.is_empty() {
|
||||
"index.html"
|
||||
} else {
|
||||
trimmed
|
||||
};
|
||||
|
||||
if let Some(file) = dir.get_file(target) {
|
||||
return file_response(file);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
#[path = "../common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use axum::http::Method;
|
||||
use common::*;
|
||||
use sandbox_agent_agent_management::testing::test_agents_from_env;
|
||||
use sandbox_agent_agent_management::agents::AgentId;
|
||||
use sandbox_agent_agent_management::testing::test_agents_from_env;
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::time::{Duration, Instant};
|
||||
use axum::http::Method;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agent_file_edit_flow() {
|
||||
|
|
@ -77,9 +77,7 @@ Do not change any other files. Reply only with DONE after editing.",
|
|||
let _ = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{session_id}/permissions/{permission_id}/reply"
|
||||
),
|
||||
&format!("/v1/sessions/{session_id}/permissions/{permission_id}/reply"),
|
||||
Some(serde_json::json!({ "reply": "once" })),
|
||||
)
|
||||
.await;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
#[path = "../common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use axum::http::Method;
|
||||
use common::*;
|
||||
use sandbox_agent_agent_management::testing::test_agents_from_env;
|
||||
use std::time::Duration;
|
||||
use axum::http::Method;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agent_permission_flow() {
|
||||
|
|
@ -41,14 +41,20 @@ async fn agent_permission_flow() {
|
|||
Some(json!({ "reply": "once" })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, axum::http::StatusCode::NO_CONTENT, "permission reply");
|
||||
assert_eq!(
|
||||
status,
|
||||
axum::http::StatusCode::NO_CONTENT,
|
||||
"permission reply"
|
||||
);
|
||||
|
||||
let resolved = poll_events_until(&app.app, &session_id, Duration::from_secs(120), |events| {
|
||||
events.iter().any(|event| {
|
||||
event.get("type").and_then(serde_json::Value::as_str) == Some("permission.resolved")
|
||||
let resolved =
|
||||
poll_events_until(&app.app, &session_id, Duration::from_secs(120), |events| {
|
||||
events.iter().any(|event| {
|
||||
event.get("type").and_then(serde_json::Value::as_str)
|
||||
== Some("permission.resolved")
|
||||
})
|
||||
})
|
||||
})
|
||||
.await;
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
resolved.iter().any(|event| {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
#[path = "../common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use axum::http::Method;
|
||||
use common::*;
|
||||
use sandbox_agent_agent_management::testing::test_agents_from_env;
|
||||
use std::time::Duration;
|
||||
use axum::http::Method;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agent_question_flow() {
|
||||
|
|
@ -44,12 +44,14 @@ async fn agent_question_flow() {
|
|||
.await;
|
||||
assert_eq!(status, axum::http::StatusCode::NO_CONTENT, "question reply");
|
||||
|
||||
let resolved = poll_events_until(&app.app, &session_id, Duration::from_secs(120), |events| {
|
||||
events.iter().any(|event| {
|
||||
event.get("type").and_then(serde_json::Value::as_str) == Some("question.resolved")
|
||||
let resolved =
|
||||
poll_events_until(&app.app, &session_id, Duration::from_secs(120), |events| {
|
||||
events.iter().any(|event| {
|
||||
event.get("type").and_then(serde_json::Value::as_str)
|
||||
== Some("question.resolved")
|
||||
})
|
||||
})
|
||||
})
|
||||
.await;
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
resolved.iter().any(|event| {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
#[path = "../common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use axum::http::Method;
|
||||
use common::*;
|
||||
use sandbox_agent_agent_management::testing::test_agents_from_env;
|
||||
use std::time::Duration;
|
||||
use axum::http::Method;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agent_termination() {
|
||||
|
|
@ -26,13 +26,20 @@ async fn agent_termination() {
|
|||
None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, axum::http::StatusCode::NO_CONTENT, "terminate session");
|
||||
assert_eq!(
|
||||
status,
|
||||
axum::http::StatusCode::NO_CONTENT,
|
||||
"terminate session"
|
||||
);
|
||||
|
||||
let events = poll_events_until(&app.app, &session_id, Duration::from_secs(30), |events| {
|
||||
has_event_type(events, "session.ended")
|
||||
})
|
||||
.await;
|
||||
assert!(has_event_type(&events, "session.ended"), "missing session.ended");
|
||||
assert!(
|
||||
has_event_type(&events, "session.ended"),
|
||||
"missing session.ended"
|
||||
);
|
||||
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
|
|
@ -41,6 +48,9 @@ async fn agent_termination() {
|
|||
Some(json!({ "message": PROMPT })),
|
||||
)
|
||||
.await;
|
||||
assert!(!status.is_success(), "terminated session should reject messages");
|
||||
assert!(
|
||||
!status.is_success(),
|
||||
"terminated session should reject messages"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
#[path = "../common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use axum::http::Method;
|
||||
use common::*;
|
||||
use sandbox_agent_agent_management::testing::test_agents_from_env;
|
||||
use serde_json::Value;
|
||||
use std::time::{Duration, Instant};
|
||||
use axum::http::Method;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agent_tool_flow() {
|
||||
|
|
@ -61,9 +61,7 @@ async fn agent_tool_flow() {
|
|||
let _ = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{session_id}/permissions/{permission_id}/reply"
|
||||
),
|
||||
&format!("/v1/sessions/{session_id}/permissions/{permission_id}/reply"),
|
||||
Some(serde_json::json!({ "reply": "once" })),
|
||||
)
|
||||
.await;
|
||||
|
|
|
|||
|
|
@ -36,11 +36,19 @@ fn test_agents_install_version_spawn() -> Result<(), Box<dyn std::error::Error>>
|
|||
let env = build_env();
|
||||
assert!(!env.is_empty(), "expected credentials to be available");
|
||||
|
||||
let agents = [AgentId::Claude, AgentId::Codex, AgentId::Opencode, AgentId::Amp];
|
||||
let agents = [
|
||||
AgentId::Claude,
|
||||
AgentId::Codex,
|
||||
AgentId::Opencode,
|
||||
AgentId::Amp,
|
||||
];
|
||||
for agent in agents {
|
||||
let install = manager.install(agent, InstallOptions::default())?;
|
||||
assert!(install.path.exists(), "expected install for {agent}");
|
||||
assert!(manager.is_installed(agent), "expected is_installed for {agent}");
|
||||
assert!(
|
||||
manager.is_installed(agent),
|
||||
"expected is_installed for {agent}"
|
||||
);
|
||||
manager.install(
|
||||
agent,
|
||||
InstallOptions {
|
||||
|
|
@ -70,9 +78,15 @@ fn test_agents_install_version_spawn() -> Result<(), Box<dyn std::error::Error>>
|
|||
);
|
||||
let combined = format!("{}{}", result.stdout, result.stderr);
|
||||
let output = result.result.clone().unwrap_or(combined);
|
||||
assert!(output.contains("OK"), "expected OK for {agent}, got: {output}");
|
||||
assert!(
|
||||
output.contains("OK"),
|
||||
"expected OK for {agent}, got: {output}"
|
||||
);
|
||||
|
||||
if agent == AgentId::Claude || agent == AgentId::Opencode || (agent == AgentId::Amp && amp_configured()) {
|
||||
if agent == AgentId::Claude
|
||||
|| agent == AgentId::Opencode
|
||||
|| (agent == AgentId::Amp && amp_configured())
|
||||
{
|
||||
let mut resume = SpawnOptions::new(prompt_ok("OK2"));
|
||||
resume.env = env.clone();
|
||||
resume.session_id = result.session_id.clone();
|
||||
|
|
@ -84,12 +98,17 @@ fn test_agents_install_version_spawn() -> Result<(), Box<dyn std::error::Error>>
|
|||
);
|
||||
let combined = format!("{}{}", resumed.stdout, resumed.stderr);
|
||||
let output = resumed.result.clone().unwrap_or(combined);
|
||||
assert!(output.contains("OK2"), "expected OK2 for {agent}, got: {output}");
|
||||
assert!(
|
||||
output.contains("OK2"),
|
||||
"expected OK2 for {agent}, got: {output}"
|
||||
);
|
||||
} else if agent == AgentId::Codex {
|
||||
let mut resume = SpawnOptions::new(prompt_ok("OK2"));
|
||||
resume.env = env.clone();
|
||||
resume.session_id = result.session_id.clone();
|
||||
let err = manager.spawn(agent, resume).expect_err("expected resume error for codex");
|
||||
let err = manager
|
||||
.spawn(agent, resume)
|
||||
.expect_err("expected resume error for codex");
|
||||
assert!(matches!(err, AgentError::ResumeUnsupported { .. }));
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +124,10 @@ fn test_agents_install_version_spawn() -> Result<(), Box<dyn std::error::Error>>
|
|||
);
|
||||
let combined = format!("{}{}", planned.stdout, planned.stderr);
|
||||
let output = planned.result.clone().unwrap_or(combined);
|
||||
assert!(output.contains("OK3"), "expected OK3 for {agent}, got: {output}");
|
||||
assert!(
|
||||
output.contains("OK3"),
|
||||
"expected OK3 for {agent}, got: {output}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,7 @@ use serde_json::{json, Value};
|
|||
use tempfile::TempDir;
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
use sandbox_agent::router::{
|
||||
build_router,
|
||||
AgentCapabilities,
|
||||
AgentListResponse,
|
||||
AuthConfig,
|
||||
};
|
||||
use sandbox_agent::router::{build_router, AgentCapabilities, AgentListResponse, AuthConfig};
|
||||
use sandbox_agent_agent_credentials::ExtractedCredentials;
|
||||
use sandbox_agent_agent_management::agents::{AgentId, AgentManager};
|
||||
|
||||
|
|
@ -32,8 +27,7 @@ pub struct TestApp {
|
|||
impl TestApp {
|
||||
pub fn new() -> Self {
|
||||
let install_dir = tempfile::tempdir().expect("create temp install dir");
|
||||
let manager = AgentManager::new(install_dir.path())
|
||||
.expect("create agent manager");
|
||||
let manager = AgentManager::new(install_dir.path()).expect("create agent manager");
|
||||
let state = sandbox_agent::router::AppState::new(AuthConfig::disabled(), manager);
|
||||
let app = build_router(state);
|
||||
Self {
|
||||
|
|
@ -59,7 +53,12 @@ impl Drop for EnvGuard {
|
|||
}
|
||||
|
||||
pub fn apply_credentials(creds: &ExtractedCredentials) -> EnvGuard {
|
||||
let keys = ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY"];
|
||||
let keys = [
|
||||
"ANTHROPIC_API_KEY",
|
||||
"CLAUDE_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"CODEX_API_KEY",
|
||||
];
|
||||
let mut saved = HashMap::new();
|
||||
for key in keys {
|
||||
saved.insert(key.to_string(), std::env::var(key).ok());
|
||||
|
|
@ -100,13 +99,11 @@ pub async fn send_json(
|
|||
.method(method)
|
||||
.uri(path)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body.map(|value| value.to_string()).unwrap_or_default()))
|
||||
.body(Body::from(
|
||||
body.map(|value| value.to_string()).unwrap_or_default(),
|
||||
))
|
||||
.expect("request");
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(request)
|
||||
.await
|
||||
.expect("response");
|
||||
let response = app.clone().oneshot(request).await.expect("response");
|
||||
let status = response.status();
|
||||
let bytes = response
|
||||
.into_body()
|
||||
|
|
@ -140,15 +137,15 @@ pub async fn install_agent(app: &Router, agent: AgentId) {
|
|||
Some(json!({})),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "install agent {}", agent.as_str());
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::NO_CONTENT,
|
||||
"install agent {}",
|
||||
agent.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn create_session(
|
||||
app: &Router,
|
||||
agent: AgentId,
|
||||
session_id: &str,
|
||||
permission_mode: &str,
|
||||
) {
|
||||
pub async fn create_session(app: &Router, agent: AgentId, session_id: &str, permission_mode: &str) {
|
||||
let status = send_status(
|
||||
app,
|
||||
Method::POST,
|
||||
|
|
|
|||
|
|
@ -28,11 +28,7 @@ async fn register_and_unregister_sessions() {
|
|||
.register_session(AgentId::Codex, "sess-1", Some("thread-1"))
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
harness
|
||||
.has_session_mapping(AgentId::Codex, "sess-1")
|
||||
.await
|
||||
);
|
||||
assert!(harness.has_session_mapping(AgentId::Codex, "sess-1").await);
|
||||
assert_eq!(
|
||||
harness
|
||||
.native_mapping(AgentId::Codex, "thread-1")
|
||||
|
|
@ -45,17 +41,11 @@ async fn register_and_unregister_sessions() {
|
|||
.unregister_session(AgentId::Codex, "sess-1", Some("thread-1"))
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
!harness
|
||||
.has_session_mapping(AgentId::Codex, "sess-1")
|
||||
.await
|
||||
);
|
||||
assert!(
|
||||
harness
|
||||
.native_mapping(AgentId::Codex, "thread-1")
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
assert!(!harness.has_session_mapping(AgentId::Codex, "sess-1").await);
|
||||
assert!(harness
|
||||
.native_mapping(AgentId::Codex, "thread-1")
|
||||
.await
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -92,9 +82,7 @@ async fn handle_process_exit_marks_error_and_ends_sessions() {
|
|||
harness
|
||||
.register_session(AgentId::Codex, "sess-1", Some("thread-1"))
|
||||
.await;
|
||||
harness
|
||||
.insert_stdio_server(AgentId::Codex, None, 1)
|
||||
.await;
|
||||
harness.insert_stdio_server(AgentId::Codex, None, 1).await;
|
||||
|
||||
harness
|
||||
.handle_process_exit(AgentId::Codex, 1, exit_status(7))
|
||||
|
|
@ -104,13 +92,11 @@ async fn handle_process_exit_marks_error_and_ends_sessions() {
|
|||
harness.server_status(AgentId::Codex).await,
|
||||
Some(sandbox_agent::router::ServerStatus::Error)
|
||||
));
|
||||
assert!(
|
||||
harness
|
||||
.server_last_error(AgentId::Codex)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.contains("exited")
|
||||
);
|
||||
assert!(harness
|
||||
.server_last_error(AgentId::Codex)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.contains("exited"));
|
||||
assert!(harness.session_ended("sess-1").await);
|
||||
assert!(matches!(
|
||||
harness.session_end_reason("sess-1").await,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
mod session_lifecycle;
|
||||
mod multi_turn;
|
||||
mod permissions;
|
||||
mod questions;
|
||||
mod reasoning;
|
||||
mod session_lifecycle;
|
||||
mod status;
|
||||
|
|
|
|||
|
|
@ -81,8 +81,13 @@ async fn multi_turn_snapshots() {
|
|||
install_agent(&app.app, config.agent).await;
|
||||
|
||||
let session_id = format!("multi-turn-{}", config.agent.as_str());
|
||||
create_session(&app.app, config.agent, &session_id, test_permission_mode(config.agent))
|
||||
.await;
|
||||
create_session(
|
||||
&app.app,
|
||||
config.agent,
|
||||
&session_id,
|
||||
test_permission_mode(config.agent),
|
||||
)
|
||||
.await;
|
||||
|
||||
send_message_with_text(&app.app, &session_id, FIRST_PROMPT).await;
|
||||
let (first_events, offset) =
|
||||
|
|
@ -100,13 +105,8 @@ async fn multi_turn_snapshots() {
|
|||
);
|
||||
|
||||
send_message_with_text(&app.app, &session_id, SECOND_PROMPT).await;
|
||||
let (second_events, _offset) = poll_events_until_from(
|
||||
&app.app,
|
||||
&session_id,
|
||||
offset,
|
||||
Duration::from_secs(120),
|
||||
)
|
||||
.await;
|
||||
let (second_events, _offset) =
|
||||
poll_events_until_from(&app.app, &session_id, offset, Duration::from_secs(120)).await;
|
||||
let second_events = truncate_after_first_stop(&second_events);
|
||||
assert!(
|
||||
!second_events.is_empty(),
|
||||
|
|
|
|||
|
|
@ -55,9 +55,7 @@ async fn permission_flow_snapshots() {
|
|||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{permission_session}/permissions/{permission_id}/reply"
|
||||
),
|
||||
&format!("/v1/sessions/{permission_session}/permissions/{permission_id}/reply"),
|
||||
Some(json!({ "reply": "once" })),
|
||||
)
|
||||
.await;
|
||||
|
|
@ -67,9 +65,7 @@ async fn permission_flow_snapshots() {
|
|||
let (status, payload) = send_json(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{permission_session}/permissions/missing-permission/reply"
|
||||
),
|
||||
&format!("/v1/sessions/{permission_session}/permissions/missing-permission/reply"),
|
||||
Some(json!({ "reply": "once" })),
|
||||
)
|
||||
.await;
|
||||
|
|
|
|||
|
|
@ -55,9 +55,7 @@ async fn question_flow_snapshots() {
|
|||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{question_reply_session}/questions/{question_id}/reply"
|
||||
),
|
||||
&format!("/v1/sessions/{question_reply_session}/questions/{question_id}/reply"),
|
||||
Some(json!({ "answers": answers })),
|
||||
)
|
||||
.await;
|
||||
|
|
@ -67,9 +65,7 @@ async fn question_flow_snapshots() {
|
|||
let (status, payload) = send_json(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{question_reply_session}/questions/missing-question/reply"
|
||||
),
|
||||
&format!("/v1/sessions/{question_reply_session}/questions/missing-question/reply"),
|
||||
Some(json!({ "answers": [] })),
|
||||
)
|
||||
.await;
|
||||
|
|
@ -92,7 +88,11 @@ async fn question_flow_snapshots() {
|
|||
Some(json!({ "message": QUESTION_PROMPT })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "send question prompt reject");
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::NO_CONTENT,
|
||||
"send question prompt reject"
|
||||
);
|
||||
|
||||
let reject_events = poll_events_until_match(
|
||||
&app.app,
|
||||
|
|
@ -108,9 +108,7 @@ async fn question_flow_snapshots() {
|
|||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{question_reject_session}/questions/{question_id}/reject"
|
||||
),
|
||||
&format!("/v1/sessions/{question_reject_session}/questions/{question_id}/reject"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
|
@ -126,7 +124,10 @@ async fn question_flow_snapshots() {
|
|||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(!status.is_success(), "missing question id reject should error");
|
||||
assert!(
|
||||
!status.is_success(),
|
||||
"missing question id reject should error"
|
||||
);
|
||||
assert_session_snapshot(
|
||||
"question_reject_missing",
|
||||
json!({
|
||||
|
|
|
|||
|
|
@ -23,8 +23,13 @@ async fn reasoning_events_present() {
|
|||
install_agent(&app.app, config.agent).await;
|
||||
|
||||
let session_id = format!("reasoning-{}", config.agent.as_str());
|
||||
create_session(&app.app, config.agent, &session_id, test_permission_mode(config.agent))
|
||||
.await;
|
||||
create_session(
|
||||
&app.app,
|
||||
config.agent,
|
||||
&session_id,
|
||||
test_permission_mode(config.agent),
|
||||
)
|
||||
.await;
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
|
|
@ -34,13 +39,11 @@ async fn reasoning_events_present() {
|
|||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "send reasoning prompt");
|
||||
|
||||
let events = poll_events_until_match(
|
||||
&app.app,
|
||||
&session_id,
|
||||
Duration::from_secs(120),
|
||||
|events| events_have_content_type(events, "reasoning") || events.iter().any(is_error_event),
|
||||
)
|
||||
.await;
|
||||
let events =
|
||||
poll_events_until_match(&app.app, &session_id, Duration::from_secs(120), |events| {
|
||||
events_have_content_type(events, "reasoning") || events.iter().any(is_error_event)
|
||||
})
|
||||
.await;
|
||||
assert!(
|
||||
events_have_content_type(&events, "reasoning"),
|
||||
"expected reasoning content for {}",
|
||||
|
|
|
|||
|
|
@ -28,8 +28,13 @@ async fn status_events_present() {
|
|||
install_agent(&app.app, config.agent).await;
|
||||
|
||||
let session_id = format!("status-{}", config.agent.as_str());
|
||||
create_session(&app.app, config.agent, &session_id, test_permission_mode(config.agent))
|
||||
.await;
|
||||
create_session(
|
||||
&app.app,
|
||||
config.agent,
|
||||
&session_id,
|
||||
test_permission_mode(config.agent),
|
||||
)
|
||||
.await;
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
|
|
@ -39,13 +44,11 @@ async fn status_events_present() {
|
|||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "send status prompt");
|
||||
|
||||
let events = poll_events_until_match(
|
||||
&app.app,
|
||||
&session_id,
|
||||
Duration::from_secs(120),
|
||||
|events| events_have_status(events) || events.iter().any(is_error_event),
|
||||
)
|
||||
.await;
|
||||
let events =
|
||||
poll_events_until_match(&app.app, &session_id, Duration::from_secs(120), |events| {
|
||||
events_have_status(events) || events.iter().any(is_error_event)
|
||||
})
|
||||
.await;
|
||||
assert!(
|
||||
events_have_status(&events),
|
||||
"expected status events for {}",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use http_body_util::BodyExt;
|
||||
use sandbox_agent_agent_management::agents::AgentManager;
|
||||
use sandbox_agent::router::{build_router, AppState, AuthConfig};
|
||||
use sandbox_agent::ui;
|
||||
use sandbox_agent_agent_management::agents::AgentManager;
|
||||
use tempfile::TempDir;
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
|
|
@ -22,10 +22,7 @@ async fn serves_inspector_ui() {
|
|||
.uri("/ui")
|
||||
.body(Body::empty())
|
||||
.expect("build request");
|
||||
let response = app
|
||||
.oneshot(request)
|
||||
.await
|
||||
.expect("request handled");
|
||||
let response = app.oneshot(request).await.expect("request handled");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(¶ms.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(¶ms.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(¶ms.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()),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue