Spike: PTY-based Claude Code integration with multi-agent concurrency
Proves that spawning `claude -p` in a pseudo-terminal from Rust gets Max subscription billing (apiKeySource: "none", rateLimitType: "five_hour") instead of per-token API charges. Concurrent agents run in parallel PTY sessions with session resumption via --resume for multi-turn conversations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
127
server/src/http/agents.rs
Normal file
127
server/src/http/agents.rs
Normal 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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,10 +61,11 @@ 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 =
|
||||
OpenApiService::new(api, "Story Kit API", "1.0").server("http://127.0.0.1:3001/api");
|
||||
OpenApiService::new(api, "Story Kit API", "1.0").server("http://127.0.0.1:3002/api");
|
||||
|
||||
let docs_api = (
|
||||
ProjectApi { ctx: ctx.clone() },
|
||||
@@ -69,11 +73,12 @@ 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 =
|
||||
OpenApiService::new(docs_api, "Story Kit API", "1.0").server("http://127.0.0.1:3001/api");
|
||||
OpenApiService::new(docs_api, "Story Kit API", "1.0").server("http://127.0.0.1:3002/api");
|
||||
|
||||
(api_service, docs_service)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user