mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 09:02:12 +00:00
acp spec (#155)
This commit is contained in:
parent
70287ec471
commit
e72eb9f611
264 changed files with 18559 additions and 51021 deletions
205
research/acp/missing-features-spec/01-questions.md
Normal file
205
research/acp/missing-features-spec/01-questions.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# 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. v2 has partial stub implementation in mock only.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
- `_sandboxagent/session/request_question` is declared as a constant in `acp_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`)
|
||||
|
||||
```rust
|
||||
#[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`)
|
||||
|
||||
```rust
|
||||
#[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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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:
|
||||
1. Look up session, take pending question by `question_id`
|
||||
2. Extract first answer: `answers.first().and_then(|inner| inner.first()).cloned()`
|
||||
3. 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_line` with `"allow"` and `updatedInput`; otherwise send `claude_tool_result_line`
|
||||
- Others: TODO
|
||||
4. Emit `QuestionResolved` event with `status: Answered`
|
||||
|
||||
### `reject_question`
|
||||
|
||||
Key flow:
|
||||
1. Look up session, take pending question
|
||||
2. Per-agent forwarding:
|
||||
- **OpenCode**: `opencode_question_reject(&agent_session_id, question_id)`
|
||||
- **Claude**: If linked to permission, send `claude_control_response_line` with `"deny"`; otherwise send `claude_tool_result_line` with `is_error: true`
|
||||
- Others: TODO
|
||||
3. Emit `QuestionResolved` event with `status: Rejected`
|
||||
|
||||
## v1 Event Flow
|
||||
|
||||
1. Agent emits `question.requested` event with `QuestionEventData { status: Requested, question_id, prompt, options }`
|
||||
2. Client renders question UI
|
||||
3. Client calls `POST /v1/sessions/{id}/questions/{qid}/reply` with `{ answers: [["selected"]] }` or `POST .../reject`
|
||||
4. System emits `question.resolved` event with `QuestionEventData { status: Answered, response: Some("...") }` or `{ status: Rejected }`
|
||||
|
||||
## v1 Agent Capability
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
1. **Agent -> Runtime:** Agent process sends `_sandboxagent/session/request_question` as a JSON-RPC request
|
||||
2. **Runtime -> Client:** Runtime forwards as a client-directed request in the SSE stream
|
||||
3. **Client -> Runtime:** Client POSTs a JSON-RPC response (answered/rejected)
|
||||
4. **Runtime -> Agent:** Runtime forwards the response back to the agent process stdin
|
||||
|
||||
### Payload Shape
|
||||
|
||||
Agent request:
|
||||
```json
|
||||
{
|
||||
"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):
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "q-1",
|
||||
"result": {
|
||||
"status": "answered",
|
||||
"answers": [["option-a"]]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Client response (rejected):
|
||||
```json
|
||||
{
|
||||
"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 |
|
||||
Loading…
Add table
Add a link
Reference in a new issue