mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 09:01:17 +00:00
6.4 KiB
6.4 KiB
Feature 1: Questions Subsystem
Implementation approach: ACP extension (_sandboxagent/session/request_question)
Summary
v1 had a full question subsystem: agent requests a question from the user, client replies with an answer or rejection, and the system tracks question status. v1 has partial stub implementation in mock only.
Current v1 State
_sandboxagent/session/request_questionis declared as a constant inacp_runtime/mod.rs:33- Advertised in capability injection (
extensions.sessionRequestQuestion: true) - Mock agent (
acp_runtime/mock.rs:174-203) emits questions when prompt contains "question" - No real agent handler in the runtime for routing question requests/responses between real agent processes and clients
- Mock response handling exists (
mock.rs:377-415) but the runtime lacks the general forwarding path
v1 Types (exact, from universal-agent-schema/src/lib.rs)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct QuestionEventData {
pub question_id: String,
pub prompt: String,
pub options: Vec<String>,
pub response: Option<String>,
pub status: QuestionStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum QuestionStatus {
Requested,
Answered,
Rejected,
}
v1 HTTP Types (exact, from router.rs)
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct QuestionReplyRequest {
pub answers: Vec<Vec<String>>,
}
#[derive(Debug, Clone)]
pub(crate) struct PendingQuestionInfo {
pub session_id: String,
pub question_id: String,
pub prompt: String,
pub options: Vec<String>,
}
#[derive(Debug, Clone)]
struct PendingQuestion {
prompt: String,
options: Vec<String>,
}
v1 HTTP Endpoints (from router.rs)
POST /v1/sessions/{session_id}/questions/{question_id}/reply -> 204 No Content
POST /v1/sessions/{session_id}/questions/{question_id}/reject -> 204 No Content
reply_question handler
async fn reply_question(
State(state): State<Arc<AppState>>,
Path((session_id, question_id)): Path<(String, String)>,
Json(request): Json<QuestionReplyRequest>,
) -> Result<StatusCode, ApiError> {
state.session_manager
.reply_question(&session_id, &question_id, request.answers)
.await?;
Ok(StatusCode::NO_CONTENT)
}
reject_question handler
async fn reject_question(
State(state): State<Arc<AppState>>,
Path((session_id, question_id)): Path<(String, String)>,
) -> Result<StatusCode, ApiError> {
state.session_manager
.reject_question(&session_id, &question_id)
.await?;
Ok(StatusCode::NO_CONTENT)
}
v1 SessionManager Methods (exact)
reply_question
Key flow:
- Look up session, take pending question by
question_id - Extract first answer:
answers.first().and_then(|inner| inner.first()).cloned() - Per-agent forwarding:
- OpenCode:
opencode_question_reply(&agent_session_id, question_id, answers) - Claude: If linked to a permission (AskUserQuestion/ExitPlanMode), send
claude_control_response_linewith"allow"andupdatedInput; otherwise sendclaude_tool_result_line - Others: TODO
- OpenCode:
- Emit
QuestionResolvedevent withstatus: Answered
reject_question
Key flow:
- Look up session, take pending question
- Per-agent forwarding:
- OpenCode:
opencode_question_reject(&agent_session_id, question_id) - Claude: If linked to permission, send
claude_control_response_linewith"deny"; otherwise sendclaude_tool_result_linewithis_error: true - Others: TODO
- OpenCode:
- Emit
QuestionResolvedevent withstatus: Rejected
v1 Event Flow
- Agent emits
question.requestedevent withQuestionEventData { status: Requested, question_id, prompt, options } - Client renders question UI
- Client calls
POST /v1/sessions/{id}/questions/{qid}/replywith{ answers: [["selected"]] }orPOST .../reject - System emits
question.resolvedevent withQuestionEventData { status: Answered, response: Some("...") }or{ status: Rejected }
v1 Agent Capability
AgentId::Claude => AgentCapabilities { questions: true, ... },
AgentId::Codex => AgentCapabilities { questions: false, ... },
AgentId::Opencode => AgentCapabilities { questions: false, ... },
AgentId::Amp => AgentCapabilities { questions: false, ... },
AgentId::Mock => AgentCapabilities { questions: true, ... },
Implementation Plan
ACP Extension Design
The question flow maps to ACP's bidirectional request/response:
- Agent -> Runtime: Agent process sends
_sandboxagent/session/request_questionas a JSON-RPC request - Runtime -> Client: Runtime forwards as a client-directed request in the SSE stream
- Client -> Runtime: Client POSTs a JSON-RPC response (answered/rejected)
- Runtime -> Agent: Runtime forwards the response back to the agent process stdin
Payload Shape
Agent request:
{
"jsonrpc": "2.0",
"id": "q-1",
"method": "_sandboxagent/session/request_question",
"params": {
"sessionId": "...",
"questionId": "uuid",
"prompt": "Which option?",
"options": [["option-a", "Option A"], ["option-b", "Option B"]]
}
}
Client response (answered):
{
"jsonrpc": "2.0",
"id": "q-1",
"result": {
"status": "answered",
"answers": [["option-a"]]
}
}
Client response (rejected):
{
"jsonrpc": "2.0",
"id": "q-1",
"result": {
"status": "rejected"
}
}
Files to Modify
| File | Change |
|---|---|
server/packages/sandbox-agent/src/acp_runtime/mod.rs |
Add real request/response forwarding for _sandboxagent/session/request_question (currently only mock) |
server/packages/sandbox-agent/src/acp_runtime/mock.rs |
Already has mock implementation; verify alignment with final payload shape |
sdks/typescript/src/client.ts |
Add onQuestion() callback and replyQuestion() / rejectQuestion() methods |
frontend/packages/inspector/ |
Add question rendering in inspector UI |
Docs to Update
| Doc | Change |
|---|---|
docs/openapi.json |
N/A (ACP extension, not HTTP endpoint) |
docs/sdks/typescript.mdx |
Document onQuestion / replyQuestion / rejectQuestion SDK methods |
docs/inspector.mdx |
Document question rendering in inspector |
research/acp/spec.md |
Update extension methods list |