feat: stream sessions and discover agent modes

This commit is contained in:
Nathan Flurry 2026-01-25 01:55:44 -08:00
parent e6b19ed2b6
commit 7b6d7ee917
8 changed files with 2763 additions and 218 deletions

File diff suppressed because it is too large Load diff

View file

@ -15,17 +15,17 @@ axum = "0.7"
clap = { version = "4.5", features = ["derive"] }
futures = "0.3"
sandbox-daemon-error = { path = "../error" }
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"] }
flate2 = "1.0"
tar = "0.4"
zip = { version = "0.6", default-features = false, features = ["deflate"] }
url = "2.5"
sandbox-daemon-agent-management = { path = "../agent-management" }
sandbox-daemon-agent-credentials = { path = "../agent-credentials" }
sandbox-daemon-universal-agent-schema = { path = "../universal-agent-schema" }
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls", "stream"] }
dirs = "5.0"
tempfile = "3.10"
time = { version = "0.3", features = ["parsing"] }
tokio = { version = "1.36", features = ["macros", "rt-multi-thread", "signal"] }
time = { version = "0.3", features = ["parsing", "formatting"] }
tokio = { version = "1.36", features = ["macros", "rt-multi-thread", "signal", "time"] }
tokio-stream = { version = "0.1", features = ["sync"] }
tower-http = { version = "0.5", features = ["cors"] }
utoipa = { version = "4.2", features = ["axum_extras"] }
schemars = "0.8"
[dev-dependencies]
tempfile = "3.10"

View file

@ -1,8 +1,10 @@
use std::io::Write;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use reqwest::blocking::Client as HttpClient;
use reqwest::Method;
use sandbox_daemon_agent_management::agents::AgentManager;
use sandbox_daemon_core::router::{
AgentInstallRequest, AppState, AuthConfig, CreateSessionRequest, MessageRequest,
PermissionReply, PermissionReplyRequest, QuestionReplyRequest,
@ -14,6 +16,8 @@ use serde_json::Value;
use thiserror::Error;
use tower_http::cors::{Any, CorsLayer};
const API_PREFIX: &str = "/v1";
#[derive(Parser, Debug)]
#[command(name = "sandbox-daemon")]
#[command(about = "Sandbox daemon for managing coding agents", version)]
@ -125,10 +129,6 @@ struct CreateSessionArgs {
model: Option<String>,
#[arg(long)]
variant: Option<String>,
#[arg(long = "agent-token")]
agent_token: Option<String>,
#[arg(long)]
validate_token: bool,
#[arg(long)]
agent_version: Option<String>,
#[command(flatten)]
@ -237,7 +237,9 @@ fn run_server(cli: &Cli) -> Result<(), CliError> {
return Err(CliError::MissingToken);
};
let state = AppState { auth };
let agent_manager =
AgentManager::new(default_install_dir()).map_err(|err| CliError::Server(err.to_string()))?;
let state = AppState::new(auth, agent_manager);
let mut router = build_router(state);
if let Some(cors) = build_cors_layer(cli)? {
@ -258,6 +260,12 @@ fn run_server(cli: &Cli) -> Result<(), CliError> {
})
}
fn default_install_dir() -> PathBuf {
dirs::data_dir()
.map(|dir| dir.join("sandbox-daemon").join("bin"))
.unwrap_or_else(|| PathBuf::from(".").join(".sandbox-daemon").join("bin"))
}
fn run_client(command: &Command, cli: &Cli) -> Result<(), CliError> {
match command {
Command::Agents(subcommand) => run_agents(&subcommand.command, cli),
@ -269,7 +277,7 @@ fn run_agents(command: &AgentsCommand, cli: &Cli) -> Result<(), CliError> {
match command {
AgentsCommand::List(args) => {
let ctx = ClientContext::new(cli, args)?;
let response = ctx.get("/agents")?;
let response = ctx.get(&format!("{API_PREFIX}/agents"))?;
print_json_response::<AgentListResponse>(response)
}
AgentsCommand::Install(args) => {
@ -277,13 +285,13 @@ fn run_agents(command: &AgentsCommand, cli: &Cli) -> Result<(), CliError> {
let body = AgentInstallRequest {
reinstall: if args.reinstall { Some(true) } else { None },
};
let path = format!("/agents/{}/install", args.agent);
let path = format!("{API_PREFIX}/agents/{}/install", args.agent);
let response = ctx.post(&path, &body)?;
print_empty_response(response)
}
AgentsCommand::Modes(args) => {
let ctx = ClientContext::new(cli, &args.client)?;
let path = format!("/agents/{}/modes", args.agent);
let path = format!("{API_PREFIX}/agents/{}/modes", args.agent);
let response = ctx.get(&path)?;
print_json_response::<AgentModesResponse>(response)
}
@ -300,11 +308,9 @@ fn run_sessions(command: &SessionsCommand, cli: &Cli) -> Result<(), CliError> {
permission_mode: args.permission_mode.clone(),
model: args.model.clone(),
variant: args.variant.clone(),
token: args.agent_token.clone(),
validate_token: if args.validate_token { Some(true) } else { None },
agent_version: args.agent_version.clone(),
};
let path = format!("/sessions/{}", args.session_id);
let path = format!("{API_PREFIX}/sessions/{}", args.session_id);
let response = ctx.post(&path, &body)?;
print_json_response::<CreateSessionResponse>(response)
}
@ -313,19 +319,19 @@ fn run_sessions(command: &SessionsCommand, cli: &Cli) -> Result<(), CliError> {
let body = MessageRequest {
message: args.message.clone(),
};
let path = format!("/sessions/{}/messages", args.session_id);
let path = format!("{API_PREFIX}/sessions/{}/messages", args.session_id);
let response = ctx.post(&path, &body)?;
print_empty_response(response)
}
SessionsCommand::GetMessages(args) | SessionsCommand::Events(args) => {
let ctx = ClientContext::new(cli, &args.client)?;
let path = format!("/sessions/{}/events", args.session_id);
let path = format!("{API_PREFIX}/sessions/{}/events", args.session_id);
let response = ctx.get_with_query(&path, &[ ("offset", args.offset), ("limit", args.limit) ])?;
print_json_response::<EventsResponse>(response)
}
SessionsCommand::EventsSse(args) => {
let ctx = ClientContext::new(cli, &args.client)?;
let path = format!("/sessions/{}/events/sse", args.session_id);
let path = format!("{API_PREFIX}/sessions/{}/events/sse", args.session_id);
let response = ctx.get_with_query(&path, &[("offset", args.offset)])?;
print_text_response(response)
}
@ -334,7 +340,7 @@ fn run_sessions(command: &SessionsCommand, cli: &Cli) -> Result<(), CliError> {
let answers: Vec<Vec<String>> = serde_json::from_str(&args.answers)?;
let body = QuestionReplyRequest { answers };
let path = format!(
"/sessions/{}/questions/{}/reply",
"{API_PREFIX}/sessions/{}/questions/{}/reply",
args.session_id, args.question_id
);
let response = ctx.post(&path, &body)?;
@ -343,7 +349,7 @@ fn run_sessions(command: &SessionsCommand, cli: &Cli) -> Result<(), CliError> {
SessionsCommand::RejectQuestion(args) => {
let ctx = ClientContext::new(cli, &args.client)?;
let path = format!(
"/sessions/{}/questions/{}/reject",
"{API_PREFIX}/sessions/{}/questions/{}/reject",
args.session_id, args.question_id
);
let response = ctx.post_empty(&path)?;
@ -355,7 +361,7 @@ fn run_sessions(command: &SessionsCommand, cli: &Cli) -> Result<(), CliError> {
reply: args.reply.clone(),
};
let path = format!(
"/sessions/{}/permissions/{}/reply",
"{API_PREFIX}/sessions/{}/permissions/{}/reply",
args.session_id, args.permission_id
);
let response = ctx.post(&path, &body)?;

File diff suppressed because it is too large Load diff