Merge spike/claude-code-integration: PTY-based Claude Code with multi-agent support

Spike proved: spawning claude -p in a PTY from Rust gets Max subscription
billing. Multi-agent concurrency confirmed with session resumption.
Includes AgentPool REST API, claude-code provider, and spike documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

# Conflicts:
#	.ignore
This commit is contained in:
Dave
2026-02-19 15:30:56 +00:00
18 changed files with 1188 additions and 15 deletions

127
server/src/http/agents.rs Normal file
View File

@@ -0,0 +1,127 @@
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
use serde::Serialize;
use std::sync::Arc;
#[derive(Tags)]
enum AgentsTags {
Agents,
}
#[derive(Object)]
struct CreateAgentPayload {
name: String,
role: String,
cwd: String,
}
#[derive(Object)]
struct SendMessagePayload {
message: String,
}
#[derive(Object, Serialize)]
struct AgentInfoResponse {
name: String,
role: String,
cwd: String,
session_id: Option<String>,
status: String,
message_count: usize,
}
#[derive(Object, Serialize)]
struct AgentMessageResponse {
agent: String,
text: String,
session_id: Option<String>,
model: Option<String>,
api_key_source: Option<String>,
rate_limit_type: Option<String>,
cost_usd: Option<f64>,
input_tokens: Option<u64>,
output_tokens: Option<u64>,
duration_ms: Option<u64>,
}
pub struct AgentsApi {
pub ctx: Arc<AppContext>,
}
#[OpenApi(tag = "AgentsTags::Agents")]
impl AgentsApi {
/// Create a new agent with a name, role, and working directory.
#[oai(path = "/agents", method = "post")]
async fn create_agent(
&self,
payload: Json<CreateAgentPayload>,
) -> OpenApiResult<Json<AgentInfoResponse>> {
let req = crate::agents::CreateAgentRequest {
name: payload.0.name,
role: payload.0.role,
cwd: payload.0.cwd,
};
let info = self.ctx.agents.create_agent(req).map_err(bad_request)?;
Ok(Json(AgentInfoResponse {
name: info.name,
role: info.role,
cwd: info.cwd,
session_id: info.session_id,
status: "idle".to_string(),
message_count: info.message_count,
}))
}
/// List all registered agents.
#[oai(path = "/agents", method = "get")]
async fn list_agents(&self) -> OpenApiResult<Json<Vec<AgentInfoResponse>>> {
let agents = self.ctx.agents.list_agents().map_err(bad_request)?;
Ok(Json(
agents
.into_iter()
.map(|info| AgentInfoResponse {
name: info.name,
role: info.role,
cwd: info.cwd,
session_id: info.session_id,
status: match info.status {
crate::agents::AgentStatus::Idle => "idle".to_string(),
crate::agents::AgentStatus::Running => "running".to_string(),
},
message_count: info.message_count,
})
.collect(),
))
}
/// Send a message to an agent and wait for its response.
#[oai(path = "/agents/:name/message", method = "post")]
async fn send_message(
&self,
name: poem_openapi::param::Path<String>,
payload: Json<SendMessagePayload>,
) -> OpenApiResult<Json<AgentMessageResponse>> {
let result = self
.ctx
.agents
.send_message(&name.0, &payload.0.message)
.await
.map_err(bad_request)?;
Ok(Json(AgentMessageResponse {
agent: result.agent,
text: result.text,
session_id: result.session_id,
model: result.model,
api_key_source: result.api_key_source,
rate_limit_type: result.rate_limit_type,
cost_usd: result.cost_usd,
input_tokens: result.input_tokens,
output_tokens: result.output_tokens,
duration_ms: result.duration_ms,
}))
}
}

View File

@@ -1,3 +1,4 @@
use crate::agents::AgentPool;
use crate::state::SessionState;
use crate::store::JsonFileStore;
use crate::workflow::WorkflowState;
@@ -9,6 +10,7 @@ pub struct AppContext {
pub state: Arc<SessionState>,
pub store: Arc<JsonFileStore>,
pub workflow: Arc<std::sync::Mutex<WorkflowState>>,
pub agents: Arc<AgentPool>,
}
pub type OpenApiResult<T> = poem::Result<T>;

View File

@@ -1,3 +1,4 @@
pub mod agents;
pub mod anthropic;
pub mod assets;
pub mod chat;
@@ -10,6 +11,7 @@ pub mod workflow;
pub mod project;
pub mod ws;
use agents::AgentsApi;
use anthropic::AnthropicApi;
use chat::ChatApi;
use context::AppContext;
@@ -45,6 +47,7 @@ type ApiTuple = (
IoApi,
ChatApi,
WorkflowApi,
AgentsApi,
);
type ApiService = OpenApiService<ApiTuple, ()>;
@@ -58,6 +61,7 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
IoApi { ctx: ctx.clone() },
ChatApi { ctx: ctx.clone() },
WorkflowApi { ctx: ctx.clone() },
AgentsApi { ctx: ctx.clone() },
);
let api_service =
@@ -69,7 +73,8 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
AnthropicApi::new(ctx.clone()),
IoApi { ctx: ctx.clone() },
ChatApi { ctx: ctx.clone() },
WorkflowApi { ctx },
WorkflowApi { ctx: ctx.clone() },
AgentsApi { ctx },
);
let docs_service =