mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 06:04:43 +00:00
acp spec
This commit is contained in:
parent
a33b1323ff
commit
2ba630c180
264 changed files with 18559 additions and 51021 deletions
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
|
|
@ -23,3 +23,13 @@ jobs:
|
|||
- run: pnpm install
|
||||
- name: Run checks
|
||||
run: ./scripts/release/main.ts --version 0.0.0 --check
|
||||
- name: Run ACP v2 server tests
|
||||
run: |
|
||||
cargo test -p sandbox-agent-agent-management
|
||||
cargo test -p sandbox-agent --test v2_api
|
||||
cargo test -p sandbox-agent --test v2_agent_process_matrix
|
||||
cargo test -p sandbox-agent --lib
|
||||
- name: Run SDK tests
|
||||
run: pnpm --dir sdks/typescript test
|
||||
- name: Run Inspector browser E2E
|
||||
run: pnpm --filter @sandbox-agent/inspector test:agent-browser
|
||||
|
|
|
|||
186
CLAUDE.md
186
CLAUDE.md
|
|
@ -1,136 +1,80 @@
|
|||
# Instructions
|
||||
|
||||
## SDK Modes
|
||||
## ACP v2 Baseline
|
||||
|
||||
There are two ways to work with the SDKs:
|
||||
- v2 is ACP-native.
|
||||
- `/v1/*` is removed and returns `410 Gone` (`application/problem+json`).
|
||||
- `/opencode/*` is disabled during ACP core phases and returns `503`.
|
||||
- Prompt/session traffic is ACP JSON-RPC over streamable HTTP on `/v2/rpc`:
|
||||
- `POST /v2/rpc`
|
||||
- `GET /v2/rpc` (SSE)
|
||||
- `DELETE /v2/rpc`
|
||||
- Control-plane endpoints:
|
||||
- `GET /v2/health`
|
||||
- `GET /v2/agents`
|
||||
- `POST /v2/agents/{agent}/install`
|
||||
- Binary filesystem transfer endpoints (intentionally HTTP, not ACP extension methods):
|
||||
- `GET /v2/fs/file`
|
||||
- `PUT /v2/fs/file`
|
||||
- `POST /v2/fs/upload-batch`
|
||||
- Sandbox Agent ACP extension method naming:
|
||||
- Custom ACP methods use `_sandboxagent/...` (not `_sandboxagent/v2/...`).
|
||||
- Session detach method is `_sandboxagent/session/detach`.
|
||||
|
||||
- **Embedded**: Spawns the `sandbox-agent` server as a subprocess on a unique port and communicates with it locally. Useful for local development or when running the SDK and agent in the same environment.
|
||||
- **Server**: Connects to a remotely running `sandbox-agent` server. The server is typically running inside a sandbox (e.g., Docker, E2B, Daytona, Vercel Sandboxes) and the SDK connects to it over HTTP.
|
||||
## API Scope
|
||||
|
||||
## Agent Schemas
|
||||
- ACP is the primary protocol for agent/session behavior and all functionality that talks directly to the agent.
|
||||
- ACP extensions may be used for gaps (for example `skills`, `models`, and related metadata), but the default is that agent-facing behavior is implemented by the agent through ACP.
|
||||
- Custom HTTP APIs are for non-agent/session platform services (for example filesystem, terminals, and other host/runtime capabilities).
|
||||
- Filesystem and terminal APIs remain Sandbox Agent-specific HTTP contracts and are not ACP.
|
||||
- Keep `GET /v2/fs/file`, `PUT /v2/fs/file`, and `POST /v2/fs/upload-batch` on HTTP:
|
||||
- These are Sandbox Agent host/runtime operations with cross-agent-consistent behavior.
|
||||
- They may involve very large binary transfers that ACP JSON-RPC envelopes are not suited to stream.
|
||||
- This is intentionally separate from ACP native `fs/read_text_file` and `fs/write_text_file`.
|
||||
- ACP extension variants may exist in parallel, but SDK defaults should prefer HTTP for these binary transfer operations.
|
||||
|
||||
Agent schemas (Claude Code, Codex, OpenCode, Amp) are available for reference in `resources/agent-schemas/artifacts/json-schema/`.
|
||||
## Naming and Ownership
|
||||
|
||||
Extraction methods:
|
||||
- **Claude**: Uses `claude --output-format json --json-schema` CLI command
|
||||
- **Codex**: Uses `codex app-server generate-json-schema` CLI command
|
||||
- **OpenCode**: Fetches from GitHub OpenAPI spec
|
||||
- **Amp**: Scrapes from `https://ampcode.com/manual/appendix?preview#message-schema`
|
||||
- This repository/product is **Sandbox Agent**.
|
||||
- **Gigacode** is a separate user-facing UI/client, not the server product name.
|
||||
- Gigacode integrates with Sandbox Agent via the OpenCode-compatible surface (`/opencode/*`) when that compatibility layer is enabled.
|
||||
- Canonical extension namespace/domain string is `sandboxagent.dev` (no hyphen).
|
||||
- Canonical custom ACP extension method prefix is `_sandboxagent/...` (no hyphen).
|
||||
|
||||
All extractors have fallback schemas for when CLI/URL is unavailable.
|
||||
## Architecture (Brief)
|
||||
|
||||
Research on how different agents operate (CLI flags, streaming formats, HITL patterns, etc.) is in `research/agents/`. When adding or making changes to agent docs, follow the same structure as existing files.
|
||||
- HTTP contract and problem/error mapping: `server/packages/sandbox-agent/src/router.rs`
|
||||
- ACP client runtime and agent process bridge: `server/packages/sandbox-agent/src/acp_runtime/mod.rs`
|
||||
- Agent/native + ACP agent process install and lazy install: `server/packages/agent-management/`
|
||||
- Inspector UI served at `/ui/` and bound to ACP over HTTP from `frontend/packages/inspector/`
|
||||
|
||||
Universal schema guidance:
|
||||
- The universal schema should cover the full feature set of all agents.
|
||||
- Conversions must be best-effort overlap without being lossy; preserve raw payloads when needed.
|
||||
- **The mock agent acts as the reference implementation** for correct event behavior. Real agents should use synthetic events to match the mock agent's event patterns (e.g., emitting both daemon synthetic and agent native `session.started` events, proper `item.started` → `item.delta` → `item.completed` sequences).
|
||||
## TypeScript SDK Architecture
|
||||
|
||||
## Spec Tracking
|
||||
- TypeScript clients are split into:
|
||||
- `acp-http-client`: protocol-pure ACP-over-HTTP (`/v2/rpc`) with no Sandbox-specific metadata/extensions.
|
||||
- `sandbox-agent`: `SandboxAgentClient` wrapper that adds Sandbox metadata/extension helpers and keeps non-ACP HTTP helpers.
|
||||
- `SandboxAgentClient` constructor is `new SandboxAgentClient(...)`.
|
||||
- `SandboxAgentClient` auto-connects by default; `autoConnect: false` requires explicit `.connect()`.
|
||||
- ACP/session methods must throw when disconnected (`NotConnectedError`), and `.connect()` must throw when already connected (`AlreadyConnectedError`).
|
||||
- A `SandboxAgentClient` instance may have at most one active ACP connection at a time.
|
||||
- Stable ACP session method names should stay ACP-aligned in the Sandbox wrapper (`newSession`, `loadSession`, `prompt`, `cancel`, `setSessionMode`, `setSessionConfigOption`).
|
||||
- Sandbox extension methods are first-class wrapper helpers (`listModels`, `setMetadata`, `detachSession`, `terminateSession`).
|
||||
|
||||
- Keep CLI subcommands in sync with every HTTP endpoint.
|
||||
- Update `CLAUDE.md` to keep CLI endpoints in sync with HTTP API changes.
|
||||
- When adding or modifying CLI commands, update `docs/cli.mdx` to reflect the changes.
|
||||
- When changing the HTTP API, update the TypeScript SDK and CLI together.
|
||||
- Do not make breaking changes to API endpoints.
|
||||
- When changing API routes, ensure the HTTP/SSE test suite has full coverage of every route.
|
||||
- When agent schema changes, ensure API tests cover the new schema and event shapes end-to-end.
|
||||
- When the universal schema changes, update mock-agent events to cover the new fields or event types.
|
||||
- Update `docs/conversion.md` whenever agent-native schema terms, synthetic events, identifier mappings, or conversion logic change.
|
||||
- Never use synthetic data or mocked responses in tests.
|
||||
- Never manually write agent types; always use generated types in `resources/agent-schemas/`. If types are broken, fix the generated types.
|
||||
- The universal schema must provide consistent behavior across providers; avoid requiring frontend/client logic to special-case agents.
|
||||
- The UI must reflect every field in AgentCapabilities (feature coverage); keep it in sync with `docs/session-transcript-schema.mdx` and `agent_capabilities_for`.
|
||||
- When parsing agent data, if something is unexpected or does not match the schema, bail out and surface the error rather than trying to continue with partial parsing.
|
||||
- When defining the universal schema, choose the option most compatible with native agent APIs, and add synthetics to fill gaps for other agents.
|
||||
- Use `docs/session-transcript-schema.mdx` as the source of truth for schema terminology and keep it updated alongside schema changes.
|
||||
- On parse failures, emit an `agent.unparsed` event (source=daemon, synthetic=true) and treat it as a test failure. Preserve raw payloads when `include_raw=true`.
|
||||
- Track subagent support in `docs/conversion.md`. For now, normalize subagent activity into normal message/tool flow, but revisit explicit subagent modeling later.
|
||||
- Keep the FAQ in `README.md` and `frontend/packages/website/src/components/FAQ.tsx` in sync. When adding or modifying FAQ entries, update both files.
|
||||
- Update `research/wip-agent-support.md` as agent support changes are implemented.
|
||||
## Source Documents
|
||||
|
||||
### OpenAPI / utoipa requirements
|
||||
- `~/misc/acp-docs/schema/schema.json`
|
||||
- `~/misc/acp-docs/schema/meta.json`
|
||||
- `research/acp/spec.md`
|
||||
- `research/acp/v1-schema-to-acp-mapping.md`
|
||||
- `research/acp/friction.md`
|
||||
- `research/acp/todo.md`
|
||||
|
||||
Every `#[utoipa::path(...)]` handler function must have a doc comment where:
|
||||
- The **first line** becomes the OpenAPI `summary` (short human-readable title, e.g. `"List Agents"`). This is used as the sidebar label and page heading in the docs site.
|
||||
- The **remaining lines** become the OpenAPI `description` (one-sentence explanation of what the endpoint does).
|
||||
- Every `responses(...)` entry must have a `description` (no empty descriptions).
|
||||
## Change Tracking
|
||||
|
||||
When adding or modifying endpoints, regenerate `docs/openapi.json` and verify titles render correctly in the docs site.
|
||||
|
||||
### CLI ⇄ HTTP endpoint map (keep in sync)
|
||||
|
||||
- `sandbox-agent api agents list` ↔ `GET /v1/agents`
|
||||
- `sandbox-agent api agents install` ↔ `POST /v1/agents/{agent}/install`
|
||||
- `sandbox-agent api agents modes` ↔ `GET /v1/agents/{agent}/modes`
|
||||
- `sandbox-agent api agents models` ↔ `GET /v1/agents/{agent}/models`
|
||||
- `sandbox-agent api sessions list` ↔ `GET /v1/sessions`
|
||||
- `sandbox-agent api sessions create` ↔ `POST /v1/sessions/{sessionId}`
|
||||
- `sandbox-agent api sessions send-message` ↔ `POST /v1/sessions/{sessionId}/messages`
|
||||
- `sandbox-agent api sessions send-message-stream` ↔ `POST /v1/sessions/{sessionId}/messages/stream`
|
||||
- `sandbox-agent api sessions terminate` ↔ `POST /v1/sessions/{sessionId}/terminate`
|
||||
- `sandbox-agent api sessions events` / `get-messages` ↔ `GET /v1/sessions/{sessionId}/events`
|
||||
- `sandbox-agent api sessions events-sse` ↔ `GET /v1/sessions/{sessionId}/events/sse`
|
||||
- `sandbox-agent api sessions reply-question` ↔ `POST /v1/sessions/{sessionId}/questions/{questionId}/reply`
|
||||
- `sandbox-agent api sessions reject-question` ↔ `POST /v1/sessions/{sessionId}/questions/{questionId}/reject`
|
||||
- `sandbox-agent api sessions reply-permission` ↔ `POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply`
|
||||
- `sandbox-agent api fs entries` ↔ `GET /v1/fs/entries`
|
||||
- `sandbox-agent api fs read` ↔ `GET /v1/fs/file`
|
||||
- `sandbox-agent api fs write` ↔ `PUT /v1/fs/file`
|
||||
- `sandbox-agent api fs delete` ↔ `DELETE /v1/fs/entry`
|
||||
- `sandbox-agent api fs mkdir` ↔ `POST /v1/fs/mkdir`
|
||||
- `sandbox-agent api fs move` ↔ `POST /v1/fs/move`
|
||||
- `sandbox-agent api fs stat` ↔ `GET /v1/fs/stat`
|
||||
- `sandbox-agent api fs upload-batch` ↔ `POST /v1/fs/upload-batch`
|
||||
|
||||
## OpenCode Compatibility Layer
|
||||
|
||||
`sandbox-agent opencode` starts a sandbox-agent server and attaches an OpenCode session (uses `/opencode`).
|
||||
|
||||
### Session ownership
|
||||
|
||||
Sessions are stored **only** in sandbox-agent's v1 `SessionManager` — they are never sent to or stored in the native OpenCode server. The OpenCode TUI reads sessions via `GET /session` which the compat layer serves from the v1 store. The native OpenCode process has no knowledge of sessions.
|
||||
|
||||
### Proxy elimination strategy
|
||||
|
||||
The `/opencode` compat layer (`opencode_compat.rs`) historically proxied many endpoints to the native OpenCode server via `proxy_native_opencode()`. The goal is to **eliminate proxying** by implementing each endpoint natively using the v1 `SessionManager` as the single source of truth.
|
||||
|
||||
**Already de-proxied** (use v1 SessionManager directly):
|
||||
- `GET /session` — `oc_session_list` reads from `SessionManager::list_sessions()`
|
||||
- `GET /session/{id}` — `oc_session_get` reads from `SessionManager::get_session_info()`
|
||||
- `GET /session/status` — `oc_session_status` derives busy/idle from v1 session `ended` flag
|
||||
- `POST /tui/open-sessions` — returns `true` directly (TUI fetches sessions from `GET /session`)
|
||||
- `POST /tui/select-session` — emits `tui.session.select` event via the OpenCode event broadcaster
|
||||
|
||||
**Still proxied** (none of these reference session IDs or the session list — all are session-agnostic):
|
||||
- `GET /command` — command list
|
||||
- `GET /config`, `PATCH /config` — project config read/write
|
||||
- `GET /global/config`, `PATCH /global/config` — global config read/write
|
||||
- `GET /tui/control/next`, `POST /tui/control/response` — TUI control loop
|
||||
- `POST /tui/append-prompt`, `/tui/submit-prompt`, `/tui/clear-prompt` — prompt management
|
||||
- `POST /tui/open-help`, `/tui/open-themes`, `/tui/open-models` — TUI navigation
|
||||
- `POST /tui/execute-command`, `/tui/show-toast`, `/tui/publish` — TUI actions
|
||||
|
||||
When converting a proxied endpoint: add needed fields to `SessionState`/`SessionInfo` in `router.rs`, implement the logic natively in `opencode_compat.rs`, and use `session_info_to_opencode_value()` to format responses.
|
||||
|
||||
## Post-Release Testing
|
||||
|
||||
After cutting a release, verify the release works correctly. Run `/project:post-release-testing` to execute the testing agent.
|
||||
|
||||
## OpenCode Compatibility Tests
|
||||
|
||||
The OpenCode compatibility suite lives at `server/packages/sandbox-agent/tests/opencode-compat` and validates the `@opencode-ai/sdk` against the `/opencode` API. Run it with:
|
||||
|
||||
```bash
|
||||
SANDBOX_AGENT_SKIP_INSPECTOR=1 pnpm --filter @sandbox-agent/opencode-compat-tests test
|
||||
```
|
||||
|
||||
## Naming
|
||||
|
||||
- The product name is "Gigacode" (capital G, lowercase c). The CLI binary/package is `gigacode` (lowercase).
|
||||
|
||||
## Git Commits
|
||||
|
||||
- Do not include any co-authors in commit messages (no `Co-Authored-By` lines)
|
||||
- Use conventional commits style (e.g., `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`)
|
||||
- Keep commit messages to a single line
|
||||
- Keep CLI subcommands and HTTP endpoints in sync.
|
||||
- Update `docs/cli.mdx` when CLI behavior changes.
|
||||
- Regenerate `docs/openapi.json` when HTTP contracts change.
|
||||
- Keep `docs/inspector.mdx` and `docs/sdks/typescript.mdx` aligned with implementation.
|
||||
- Append blockers/decisions to `research/acp/friction.md` during ACP work.
|
||||
- TypeScript SDK tests should run against a real running server/runtime over real `/v2` HTTP APIs, typically using the real `mock` agent for deterministic behavior.
|
||||
- Do not use Vitest fetch/transport mocks to simulate server functionality in TypeScript SDK tests.
|
||||
|
|
|
|||
12
Cargo.toml
12
Cargo.toml
|
|
@ -3,7 +3,7 @@ resolver = "2"
|
|||
members = ["server/packages/*", "gigacode"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.12-rc.1"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
authors = [ "Rivet Gaming, LLC <developer@rivet.gg>" ]
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -12,12 +12,10 @@ description = "Universal API for automatic coding agents in sandboxes. Supports
|
|||
|
||||
[workspace.dependencies]
|
||||
# Internal crates
|
||||
sandbox-agent = { version = "0.1.12-rc.1", path = "server/packages/sandbox-agent" }
|
||||
sandbox-agent-error = { version = "0.1.12-rc.1", path = "server/packages/error" }
|
||||
sandbox-agent-agent-management = { version = "0.1.12-rc.1", path = "server/packages/agent-management" }
|
||||
sandbox-agent-agent-credentials = { version = "0.1.12-rc.1", path = "server/packages/agent-credentials" }
|
||||
sandbox-agent-universal-agent-schema = { version = "0.1.12-rc.1", path = "server/packages/universal-agent-schema" }
|
||||
sandbox-agent-extracted-agent-schemas = { version = "0.1.12-rc.1", path = "server/packages/extracted-agent-schemas" }
|
||||
sandbox-agent = { version = "0.2.0", path = "server/packages/sandbox-agent" }
|
||||
sandbox-agent-error = { version = "0.2.0", path = "server/packages/error" }
|
||||
sandbox-agent-agent-management = { version = "0.2.0", path = "server/packages/agent-management" }
|
||||
sandbox-agent-agent-credentials = { version = "0.2.0", path = "server/packages/agent-credentials" }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
|
|||
67
docs/advanced/acp-http-client.mdx
Normal file
67
docs/advanced/acp-http-client.mdx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
title: "ACP HTTP Client"
|
||||
description: "Protocol-pure ACP JSON-RPC over streamable HTTP client."
|
||||
---
|
||||
|
||||
`acp-http-client` is a standalone, low-level package for ACP over HTTP (`/v2/rpc`).
|
||||
|
||||
Use it when you want strict ACP protocol behavior with no Sandbox-specific metadata or extension adaptation.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install acp-http-client
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { AcpHttpClient } from "acp-http-client";
|
||||
|
||||
const client = new AcpHttpClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
|
||||
await client.initialize({
|
||||
_meta: {
|
||||
"sandboxagent.dev": {
|
||||
agent: "mock",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const session = await client.newSession({
|
||||
cwd: "/",
|
||||
mcpServers: [],
|
||||
_meta: {
|
||||
"sandboxagent.dev": {
|
||||
agent: "mock",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await client.prompt({
|
||||
sessionId: session.sessionId,
|
||||
prompt: [{ type: "text", text: "hello" }],
|
||||
});
|
||||
|
||||
console.log(result.stopReason);
|
||||
await client.disconnect();
|
||||
```
|
||||
|
||||
## Scope
|
||||
|
||||
- Implements ACP HTTP transport and connection lifecycle.
|
||||
- Supports ACP requests/notifications and session streaming.
|
||||
- Does not inject `_meta["sandboxagent.dev"]`.
|
||||
- Does not wrap `_sandboxagent/*` extension methods/events.
|
||||
|
||||
## Transport Contract
|
||||
|
||||
- `POST /v2/rpc` is JSON-only. Send `Content-Type: application/json` and `Accept: application/json`.
|
||||
- `GET /v2/rpc` is SSE-only. Send `Accept: text/event-stream`.
|
||||
- Keep one active SSE stream per ACP connection id.
|
||||
- `x-acp-agent` is removed. Provide agent via `_meta["sandboxagent.dev"].agent` on `initialize` and `session/new`.
|
||||
|
||||
If you want Sandbox Agent metadata/extensions and higher-level helpers, use `sandbox-agent` and `SandboxAgentClient` instead.
|
||||
|
|
@ -24,12 +24,13 @@ Sessions are the unit of interaction with an agent. You create one session per t
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.createSession("build-session", {
|
||||
agent: "codex",
|
||||
|
|
@ -60,12 +61,13 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session" \
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.postMessage("build-session", {
|
||||
message: "Summarize the repository structure.",
|
||||
|
|
@ -84,12 +86,13 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/messages" \
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
const response = await client.postMessageStream("build-session", {
|
||||
message: "Explain the main entrypoints.",
|
||||
|
|
@ -118,12 +121,13 @@ curl -N -X POST "http://127.0.0.1:2468/v1/sessions/build-session/messages/stream
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
const events = await client.getEvents("build-session", {
|
||||
offset: 0,
|
||||
|
|
@ -146,12 +150,13 @@ curl -X GET "http://127.0.0.1:2468/v1/sessions/build-session/events?offset=0&lim
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
for await (const event of client.streamEvents("build-session", { offset: 0 })) {
|
||||
console.log(event.type, event.data);
|
||||
|
|
@ -168,12 +173,13 @@ curl -N -X GET "http://127.0.0.1:2468/v1/sessions/build-session/events/sse?offse
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
const sessions = await client.listSessions();
|
||||
console.log(sessions.sessions);
|
||||
|
|
@ -191,12 +197,13 @@ When the agent asks a question, reply with an array of answers. Each inner array
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.replyQuestion("build-session", "question-1", {
|
||||
answers: [["yes"]],
|
||||
|
|
@ -215,12 +222,13 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/questions/question
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.rejectQuestion("build-session", "question-1");
|
||||
```
|
||||
|
|
@ -237,12 +245,13 @@ Use `once`, `always`, or `reject`.
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.replyPermission("build-session", "permission-1", {
|
||||
reply: "once",
|
||||
|
|
@ -261,12 +270,13 @@ curl -X POST "http://127.0.0.1:2468/v1/sessions/build-session/permissions/permis
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.terminateSession("build-session");
|
||||
```
|
||||
|
|
|
|||
|
|
@ -11,13 +11,14 @@ Use the filesystem API to upload files, then reference them as attachments when
|
|||
<Step title="Upload a file">
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
import fs from "node:fs";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
const buffer = await fs.promises.readFile("./data.csv");
|
||||
|
||||
|
|
@ -42,12 +43,13 @@ Use the filesystem API to upload files, then reference them as attachments when
|
|||
<Step title="Attach the file in a prompt">
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.postMessage("my-session", {
|
||||
message: "Please analyze the attached CSV.",
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
# Universal ↔ Agent Term Mapping
|
||||
|
||||
Source of truth: generated agent schemas in `resources/agent-schemas/artifacts/json-schema/`.
|
||||
|
||||
Identifiers
|
||||
|
||||
+----------------------+------------------------+------------------------------------------+-----------------------------+------------------------+----------------------+
|
||||
| Universal term | Claude | Codex (app-server) | OpenCode | Amp | Pi (RPC) |
|
||||
+----------------------+------------------------+------------------------------------------+-----------------------------+------------------------+----------------------+
|
||||
| session_id | n/a (daemon-only) | n/a (daemon-only) | n/a (daemon-only) | n/a (daemon-only) | n/a (daemon-only) |
|
||||
| native_session_id | none | threadId | sessionID | none | sessionId |
|
||||
| item_id | synthetic | ThreadItem.id | Message.id | StreamJSONMessage.id | messageId/toolCallId |
|
||||
| native_item_id | none | ThreadItem.id | Message.id | StreamJSONMessage.id | messageId/toolCallId |
|
||||
+----------------------+------------------------+------------------------------------------+-----------------------------+------------------------+----------------------+
|
||||
|
||||
Notes:
|
||||
- When a provider does not supply IDs (Claude), we synthesize item_id values and keep native_item_id null.
|
||||
- native_session_id is the only provider session identifier. It is intentionally used for thread/session/run ids.
|
||||
- native_item_id preserves the agent-native item/message id when present.
|
||||
- source indicates who emitted the event: agent (native) or daemon (synthetic).
|
||||
- raw is always present on events. When clients do not opt-in to raw payloads, raw is null.
|
||||
- opt-in via `include_raw=true` on events endpoints (HTTP + SSE).
|
||||
- If parsing fails, emit agent.unparsed (source=daemon, synthetic=true). Tests must assert zero unparsed events.
|
||||
|
||||
Runtime model by agent
|
||||
|
||||
| Agent | Runtime model | Notes |
|
||||
|---|---|---|
|
||||
| Claude | Per-message subprocess streaming | Routed through `AgentManager::spawn_streaming` with Claude stream-json stdin. |
|
||||
| Amp | Per-message subprocess streaming | Routed through `AgentManager::spawn_streaming` with parsed JSONL output. |
|
||||
| Codex | Shared app-server (stdio JSON-RPC) | One shared server process, daemon sessions map to Codex thread IDs. |
|
||||
| OpenCode | Shared HTTP server + SSE | One shared HTTP server, daemon sessions map to OpenCode session IDs. |
|
||||
| Pi | Dedicated per-session RPC process | Canonical path is router-managed Pi runtime (`pi --mode rpc`), one process per daemon session. |
|
||||
|
||||
Pi runtime contract:
|
||||
- Session/message lifecycle for Pi must stay on router-managed per-session RPC runtime.
|
||||
- `AgentManager::spawn(Pi)` is kept for one-shot utility/testing flows.
|
||||
- `AgentManager::spawn_streaming(Pi)` is intentionally unsupported.
|
||||
|
||||
Events / Message Flow
|
||||
|
||||
+------------------------+------------------------------+--------------------------------------------+-----------------------------------------+----------------------------------+----------------------------+
|
||||
| Universal term | Claude | Codex (app-server) | OpenCode | Amp | Pi (RPC) |
|
||||
+------------------------+------------------------------+--------------------------------------------+-----------------------------------------+----------------------------------+----------------------------+
|
||||
| session.started | none | method=thread/started | type=session.created | none | none |
|
||||
| session.ended | SDKMessage.type=result | no explicit session end (turn/completed) | no explicit session end (session.deleted)| type=done | none (daemon synthetic) |
|
||||
| turn.started | synthetic on message send | method=turn/started | type=session.status (busy) | synthetic on message send | none (daemon synthetic) |
|
||||
| turn.ended | synthetic after result | method=turn/completed | type=session.idle | synthetic on done | none (daemon synthetic) |
|
||||
| message (user) | SDKMessage.type=user | item/completed (ThreadItem.type=userMessage)| message.updated (Message.role=user) | type=message | none (daemon synthetic) |
|
||||
| message (assistant) | SDKMessage.type=assistant | item/completed (ThreadItem.type=agentMessage)| message.updated (Message.role=assistant)| type=message | message_start/message_end |
|
||||
| message.delta | stream_event (partial) or synthetic | method=item/agentMessage/delta | type=message.part.updated (text-part delta) | synthetic | message_update (text_delta/thinking_delta) |
|
||||
| tool call | type=tool_use | method=item/mcpToolCall/progress | message.part.updated (part.type=tool) | type=tool_call | tool_execution_start |
|
||||
| tool result | user.message.content.tool_result | item/completed (tool result ThreadItem variants) | message.part.updated (part.type=tool, state=completed) | type=tool_result | tool_execution_end |
|
||||
| permission.requested | control_request.can_use_tool | none | type=permission.asked | none | none |
|
||||
| permission.resolved | daemon reply to can_use_tool | none | type=permission.replied | none | none |
|
||||
| question.requested | tool_use (AskUserQuestion) | experimental request_user_input (payload) | type=question.asked | none | none |
|
||||
| question.resolved | tool_result (AskUserQuestion) | experimental request_user_input (payload) | type=question.replied / question.rejected | none | none |
|
||||
| error | SDKResultMessage.error | method=error | type=session.error (or message error) | type=error | hook_error (status item) |
|
||||
+------------------------+------------------------------+--------------------------------------------+-----------------------------------------+----------------------------------+----------------------------+
|
||||
|
||||
Permission status normalization:
|
||||
- `permission.requested` uses `status=requested`.
|
||||
- `permission.resolved` uses `status=accept`, `accept_for_session`, or `reject`.
|
||||
|
||||
Synthetics
|
||||
|
||||
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
|
||||
| Synthetic element | When it appears | Stored as | Notes |
|
||||
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
|
||||
| session.started | When agent emits no explicit start | session.started event | Mark source=daemon |
|
||||
| session.ended | When agent emits no explicit end | session.ended event | Mark source=daemon; reason may be inferred |
|
||||
| turn.started | When agent emits no explicit turn start | turn.started event | Mark source=daemon |
|
||||
| turn.ended | When agent emits no explicit turn end | turn.ended event | Mark source=daemon |
|
||||
| item_id (Claude) | Claude provides no item IDs | item_id | Maintain provider_item_id map when possible |
|
||||
| user message (Claude) | Claude emits only assistant output | item.completed | Mark source=daemon; preserve raw input in event metadata |
|
||||
| question events (Claude) | AskUserQuestion tool usage | question.requested/resolved | Derived from tool_use blocks (source=agent) |
|
||||
| native_session_id (Codex) | Codex uses threadId | native_session_id | Intentionally merged threadId into native_session_id |
|
||||
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
|
||||
| message.delta (Claude) | No native deltas emitted | item.delta | Synthetic delta with full message content; source=daemon |
|
||||
| message.delta (Amp) | No native deltas | item.delta | Synthetic delta with full message content; source=daemon |
|
||||
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
|
||||
| message.delta (OpenCode) | text part delta before message | item.delta | If part arrives first, create item.started stub then delta |
|
||||
+------------------------------+------------------------+--------------------------+--------------------------------------------------------------+
|
||||
|
||||
Delta handling
|
||||
|
||||
- Codex emits agent message and other deltas (e.g., item/agentMessage/delta).
|
||||
- OpenCode emits part deltas via message.part.updated with a delta string.
|
||||
- Claude can emit stream_event deltas when partial streaming is enabled; Amp does not emit deltas.
|
||||
- Pi emits message_update deltas and cumulative tool_execution_update partialResult values (we diff to produce deltas).
|
||||
|
||||
Policy:
|
||||
- Emit item.delta for streamable text content across providers.
|
||||
- For providers without native deltas, emit a single synthetic delta containing the full content prior to item.completed.
|
||||
- For Claude when partial streaming is enabled, forward native deltas and skip the synthetic full-content delta.
|
||||
- For providers with native deltas, forward as-is; also emit item.completed when final content is known.
|
||||
- For OpenCode reasoning part deltas, emit typed reasoning item updates (item.started/item.completed with content.type=reasoning) instead of item.delta.
|
||||
|
||||
Message normalization notes
|
||||
|
||||
- user vs assistant: normalized via role in the universal item; provider role fields or item types determine role.
|
||||
- file artifacts: always represented as content parts (type=file_ref) inside message/tool_result items, not a separate item kind.
|
||||
- reasoning: represented as content parts (type=reasoning) inside message items, with visibility when available.
|
||||
- subagents: OpenCode subtask parts and Claude Task tool usage are currently normalized into standard message/tool flow (no dedicated subagent fields).
|
||||
- OpenCode unrolling: message.updated creates/updates the parent message item; tool-related parts emit separate tool item events (item.started/ item.completed) with parent_id pointing to the message item.
|
||||
- If a message.part.updated arrives before message.updated, we create a stub item.started (source=daemon) so deltas have a parent.
|
||||
- Tool calls/results are always emitted as separate tool items to keep behavior consistent across agents.
|
||||
- If Pi message_update events omit messageId, we synthesize a stable message id and emit a synthetic item.started before the first delta so streaming text stays grouped.
|
||||
- Pi auto_compaction_start/auto_compaction_end and auto_retry_start/auto_retry_end events are mapped to status items (label `pi.*`).
|
||||
- Pi extension_ui_request/extension_error events are mapped to status items.
|
||||
- Pi RPC from pi-coding-agent does not include sessionId in events; each daemon session owns a dedicated Pi RPC process, so events are routed by runtime ownership (parallel sessions supported).
|
||||
- PI `variant` maps directly to PI RPC `set_thinking_level.level` before prompts are sent.
|
||||
- PI remains source of truth for thinking-level constraints: unsupported levels (including non-reasoning models and model-specific limits such as `xhigh`) are PI-native clamped or rejected.
|
||||
|
|
@ -1,144 +1,55 @@
|
|||
---
|
||||
title: "Credentials"
|
||||
description: "How sandbox-agent discovers and uses provider credentials."
|
||||
description: "How sandbox-agent discovers and exposes provider credentials."
|
||||
icon: "key"
|
||||
---
|
||||
|
||||
Sandbox-agent automatically discovers API credentials from environment variables and agent config files. Credentials are used to authenticate with AI providers (Anthropic, OpenAI) when spawning agents.
|
||||
`sandbox-agent` can discover provider credentials from environment variables and local agent config files.
|
||||
|
||||
## Credential sources
|
||||
## Supported providers
|
||||
|
||||
Credentials are extracted in priority order. The first valid credential found for each provider is used.
|
||||
- Anthropic
|
||||
- OpenAI
|
||||
- Additional provider entries discovered via OpenCode config
|
||||
|
||||
### Environment variables (highest priority)
|
||||
|
||||
**API keys** (checked first):
|
||||
## Common environment variables
|
||||
|
||||
| Variable | Provider |
|
||||
|----------|----------|
|
||||
| --- | --- |
|
||||
| `ANTHROPIC_API_KEY` | Anthropic |
|
||||
| `CLAUDE_API_KEY` | Anthropic (fallback) |
|
||||
| `CLAUDE_API_KEY` | Anthropic fallback |
|
||||
| `OPENAI_API_KEY` | OpenAI |
|
||||
| `CODEX_API_KEY` | OpenAI (fallback) |
|
||||
| `CODEX_API_KEY` | OpenAI fallback |
|
||||
|
||||
**OAuth tokens** (checked if no API key found):
|
||||
## Extract credentials (CLI)
|
||||
|
||||
| Variable | Provider |
|
||||
|----------|----------|
|
||||
| `CLAUDE_CODE_OAUTH_TOKEN` | Anthropic (OAuth) |
|
||||
| `ANTHROPIC_AUTH_TOKEN` | Anthropic (OAuth fallback) |
|
||||
|
||||
OAuth tokens from environment variables are only used when `include_oauth` is enabled (the default).
|
||||
|
||||
### Agent config files
|
||||
|
||||
If no environment variable is set, sandbox-agent checks agent-specific config files:
|
||||
|
||||
| Agent | Config path | Provider |
|
||||
|-------|-------------|----------|
|
||||
| Amp | `~/.amp/config.json` | Anthropic |
|
||||
| Claude Code | `~/.claude.json`, `~/.claude/.credentials.json` | Anthropic |
|
||||
| Codex | `~/.codex/auth.json` | OpenAI |
|
||||
| OpenCode | `~/.local/share/opencode/auth.json` | Both |
|
||||
|
||||
OAuth tokens are supported for Claude Code, Codex, and OpenCode. Expired tokens are automatically skipped.
|
||||
|
||||
## Provider requirements by agent
|
||||
|
||||
| Agent | Required provider |
|
||||
|-------|-------------------|
|
||||
| Claude Code | Anthropic |
|
||||
| Amp | Anthropic |
|
||||
| Codex | OpenAI |
|
||||
| OpenCode | Anthropic or OpenAI |
|
||||
| Mock | None |
|
||||
|
||||
## Error handling behavior
|
||||
|
||||
Sandbox-agent uses a **best-effort, fail-forward** approach to credentials:
|
||||
|
||||
### Extraction failures are silent
|
||||
|
||||
If a config file is missing, unreadable, or malformed, extraction continues to the next source. No errors are thrown. Missing credentials simply mean the provider is marked as unavailable.
|
||||
|
||||
```
|
||||
~/.claude.json missing → try ~/.claude/.credentials.json
|
||||
~/.claude/.credentials.json missing → try OpenCode config
|
||||
All sources exhausted → anthropic = None (not an error)
|
||||
```
|
||||
|
||||
### Agents spawn without credential validation
|
||||
|
||||
When you send a message to a session, sandbox-agent does **not** pre-validate credentials. The agent process is spawned with whatever credentials were found (or none), and the agent's native error surfaces if authentication fails.
|
||||
|
||||
This design:
|
||||
- Lets you test agent error handling behavior
|
||||
- Avoids duplicating provider-specific auth validation
|
||||
- Ensures sandbox-agent faithfully proxies agent behavior
|
||||
|
||||
For example, sending a message to Claude Code without Anthropic credentials will spawn the agent, which will then emit its own "ANTHROPIC_API_KEY not set" error through the event stream.
|
||||
|
||||
## Checking credential status
|
||||
|
||||
### API endpoint
|
||||
|
||||
The `GET /v1/agents` endpoint includes a `credentialsAvailable` field for each agent:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"id": "claude",
|
||||
"installed": true,
|
||||
"credentialsAvailable": true,
|
||||
...
|
||||
},
|
||||
{
|
||||
"id": "codex",
|
||||
"installed": true,
|
||||
"credentialsAvailable": false,
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript SDK
|
||||
|
||||
```typescript
|
||||
const { agents } = await client.listAgents();
|
||||
for (const agent of agents) {
|
||||
console.log(`${agent.id}: ${agent.credentialsAvailable ? 'authenticated' : 'no credentials'}`);
|
||||
}
|
||||
```
|
||||
|
||||
### OpenCode compatibility
|
||||
|
||||
The `/opencode/provider` endpoint returns a `connected` array listing providers with valid credentials:
|
||||
|
||||
```json
|
||||
{
|
||||
"all": [...],
|
||||
"connected": ["claude", "mock"]
|
||||
}
|
||||
```
|
||||
|
||||
## Passing credentials explicitly
|
||||
|
||||
You can override auto-discovered credentials by setting environment variables before starting sandbox-agent:
|
||||
Show discovered credentials (redacted by default):
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-...
|
||||
export OPENAI_API_KEY=sk-...
|
||||
sandbox-agent daemon start
|
||||
sandbox-agent credentials extract
|
||||
```
|
||||
|
||||
Or when using the SDK in embedded mode:
|
||||
Reveal raw values:
|
||||
|
||||
```typescript
|
||||
const client = await SandboxAgentClient.spawn({
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: process.env.MY_ANTHROPIC_KEY,
|
||||
},
|
||||
});
|
||||
```bash
|
||||
sandbox-agent credentials extract --reveal
|
||||
```
|
||||
|
||||
Filter by agent/provider:
|
||||
|
||||
```bash
|
||||
sandbox-agent credentials extract --agent codex
|
||||
sandbox-agent credentials extract --provider openai
|
||||
```
|
||||
|
||||
Emit shell exports:
|
||||
|
||||
```bash
|
||||
sandbox-agent credentials extract-env --export
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Discovery is best-effort: missing/invalid files do not crash extraction.
|
||||
- v2 does not expose legacy v1 `credentialsAvailable` agent fields.
|
||||
- Authentication failures are surfaced by the selected ACP agent process/agent during ACP requests.
|
||||
|
|
|
|||
|
|
@ -66,13 +66,14 @@ Both approaches execute code inside the sandbox, so your tools have full access
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
import fs from "node:fs";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
const content = await fs.promises.readFile("./dist/mcp-server.cjs");
|
||||
await client.writeFsFile(
|
||||
|
|
@ -175,13 +176,14 @@ Skills are markdown files that instruct the agent how to use a script. Upload th
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
import fs from "node:fs";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
const script = await fs.promises.readFile("./dist/random-number.cjs");
|
||||
await client.writeFsFile(
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const PORT = 8000;
|
|||
/** Check if sandbox-agent is already running */
|
||||
async function isServerRunning(sandbox: Sandbox): Promise<boolean> {
|
||||
try {
|
||||
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v1/health`);
|
||||
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v2/health`);
|
||||
return result.success;
|
||||
} catch {
|
||||
return false;
|
||||
|
|
@ -131,7 +131,7 @@ export default {
|
|||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Proxy requests: /sandbox/:name/v1/...
|
||||
// Proxy requests: /sandbox/:name/v2/...
|
||||
const match = url.pathname.match(/^\/sandbox\/([^/]+)(\/.*)?$/);
|
||||
if (match) {
|
||||
const [, name, path = "/"] = match;
|
||||
|
|
@ -154,11 +154,12 @@ export default {
|
|||
## Connect from Client
|
||||
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
// Connect via the proxy endpoint
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://localhost:8787/sandbox/my-sandbox",
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
// Wait for server to be ready
|
||||
|
|
@ -230,7 +231,7 @@ First run builds the Docker container (2-3 minutes). Subsequent runs are much fa
|
|||
Test with curl:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8787/sandbox/demo/v1/health
|
||||
curl http://localhost:8787/sandbox/demo/v2/health
|
||||
```
|
||||
|
||||
<Tip>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Daytona Tier 3+ is required to access api.anthropic.com and api.openai.com. Tier
|
|||
|
||||
```typescript
|
||||
import { Daytona } from "@daytonaio/sdk";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const daytona = new Daytona();
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ await new Promise((r) => setTimeout(r, 2000));
|
|||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||
|
||||
// Connect and use the SDK
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
|
||||
|
||||
await client.createSession("my-session", {
|
||||
agent: "claude",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Access the API at `http://localhost:3000`.
|
|||
|
||||
```typescript
|
||||
import Docker from "dockerode";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const docker = new Docker();
|
||||
const PORT = 3000;
|
||||
|
|
@ -62,7 +62,7 @@ await container.start();
|
|||
|
||||
// Wait for server and connect
|
||||
const baseUrl = `http://127.0.0.1:${PORT}`;
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
|
||||
|
||||
// Use the client...
|
||||
await client.createSession("my-session", {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ description: "Deploy the daemon inside an E2B sandbox."
|
|||
|
||||
```typescript
|
||||
import { Sandbox } from "@e2b/code-interpreter";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
// Pass API keys to the sandbox
|
||||
const envs: Record<string, string> = {};
|
||||
|
|
@ -38,7 +38,7 @@ await sandbox.commands.run(
|
|||
|
||||
// Connect to the server
|
||||
const baseUrl = `https://${sandbox.getHost(3000)}`;
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
|
||||
|
||||
// Wait for server to be ready
|
||||
for (let i = 0; i < 30; i++) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ description: "Deploy the daemon inside a Vercel Sandbox."
|
|||
|
||||
```typescript
|
||||
import { Sandbox } from "@vercel/sandbox";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
// Pass API keys to the sandbox
|
||||
const envs: Record<string, string> = {};
|
||||
|
|
@ -51,7 +51,7 @@ await sandbox.runCommand({
|
|||
|
||||
// Connect to the server
|
||||
const baseUrl = sandbox.domain(3000);
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
const client = new SandboxAgentClient({ baseUrl, agent: "mock" });
|
||||
|
||||
// Wait for server to be ready
|
||||
for (let i = 0; i < 30; i++) {
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@
|
|||
"group": "Features",
|
||||
"pages": ["file-system"]
|
||||
},
|
||||
{
|
||||
"group": "Advanced",
|
||||
"pages": ["advanced/acp-http-client"]
|
||||
},
|
||||
{
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,17 @@ icon: "folder"
|
|||
---
|
||||
|
||||
The filesystem API lets you list, read, write, move, and delete files inside the sandbox, plus upload batches of files via tar archives.
|
||||
Control operations (`list`, `mkdir`, `move`, `stat`, `delete`) are ACP extensions on `/v2/rpc` and require an active ACP connection in the SDK.
|
||||
|
||||
Binary transfer is intentionally a separate HTTP API (not ACP extension methods):
|
||||
|
||||
- `GET /v2/fs/file`
|
||||
- `PUT /v2/fs/file`
|
||||
- `POST /v2/fs/upload-batch`
|
||||
|
||||
Reason: these are host/runtime capabilities implemented by Sandbox Agent for cross-agent-consistent behavior, and they may require streaming very large binary payloads that ACP JSON-RPC is not suited to transport efficiently.
|
||||
This is intentionally separate from ACP native `fs/read_text_file` and `fs/write_text_file`.
|
||||
ACP extension variants may exist in parallel for compatibility, but SDK defaults should use the HTTP endpoints above for binary transfer.
|
||||
|
||||
## Path Resolution
|
||||
|
||||
|
|
@ -18,14 +29,15 @@ The session working directory is the server process current working directory at
|
|||
|
||||
## List Entries
|
||||
|
||||
`listFsEntries()` uses ACP extension method `_sandboxagent/fs/list_entries`.
|
||||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock" });
|
||||
|
||||
const entries = await client.listFsEntries({
|
||||
path: "./workspace",
|
||||
|
|
@ -36,23 +48,25 @@ console.log(entries);
|
|||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X GET "http://127.0.0.1:2468/v1/fs/entries?path=./workspace&sessionId=my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "x-acp-connection-id: acp_conn_1" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"_sandboxagent/fs/list_entries","params":{"path":"./workspace","sessionId":"my-session"}}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Read And Write Files
|
||||
|
||||
`PUT /v1/fs/file` writes raw bytes. `GET /v1/fs/file` returns raw bytes.
|
||||
`PUT /v2/fs/file` writes raw bytes. `GET /v2/fs/file` returns raw bytes.
|
||||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock" });
|
||||
|
||||
await client.writeFsFile({ path: "./notes.txt", sessionId: "my-session" }, "hello");
|
||||
|
||||
|
|
@ -66,11 +80,11 @@ console.log(text);
|
|||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt&sessionId=my-session" \
|
||||
curl -X PUT "http://127.0.0.1:2468/v2/fs/file?path=./notes.txt&sessionId=my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
--data-binary "hello"
|
||||
|
||||
curl -X GET "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt&sessionId=my-session" \
|
||||
curl -X GET "http://127.0.0.1:2468/v2/fs/file?path=./notes.txt&sessionId=my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
--output ./notes.txt
|
||||
```
|
||||
|
|
@ -78,14 +92,15 @@ curl -X GET "http://127.0.0.1:2468/v1/fs/file?path=./notes.txt&sessionId=my-sess
|
|||
|
||||
## Create Directories
|
||||
|
||||
`mkdirFs()` uses ACP extension method `_sandboxagent/fs/mkdir`.
|
||||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock" });
|
||||
|
||||
await client.mkdirFs({
|
||||
path: "./data",
|
||||
|
|
@ -94,21 +109,25 @@ await client.mkdirFs({
|
|||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "http://127.0.0.1:2468/v1/fs/mkdir?path=./data&sessionId=my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "x-acp-connection-id: acp_conn_1" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","id":2,"method":"_sandboxagent/fs/mkdir","params":{"path":"./data","sessionId":"my-session"}}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Move, Delete, And Stat
|
||||
|
||||
`moveFs()`, `statFs()`, and `deleteFsEntry()` use ACP extension methods (`_sandboxagent/fs/move`, `_sandboxagent/fs/stat`, `_sandboxagent/fs/delete_entry`).
|
||||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock" });
|
||||
|
||||
await client.moveFs(
|
||||
{ from: "./notes.txt", to: "./notes-old.txt", overwrite: true },
|
||||
|
|
@ -129,16 +148,23 @@ console.log(stat);
|
|||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "http://127.0.0.1:2468/v1/fs/move?sessionId=my-session" \
|
||||
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "x-acp-connection-id: acp_conn_1" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"from":"./notes.txt","to":"./notes-old.txt","overwrite":true}'
|
||||
-d '{"jsonrpc":"2.0","id":3,"method":"_sandboxagent/fs/move","params":{"from":"./notes.txt","to":"./notes-old.txt","overwrite":true,"sessionId":"my-session"}}'
|
||||
|
||||
curl -X GET "http://127.0.0.1:2468/v1/fs/stat?path=./notes-old.txt&sessionId=my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "x-acp-connection-id: acp_conn_1" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","id":4,"method":"_sandboxagent/fs/stat","params":{"path":"./notes-old.txt","sessionId":"my-session"}}'
|
||||
|
||||
curl -X DELETE "http://127.0.0.1:2468/v1/fs/entry?path=./notes-old.txt&sessionId=my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN"
|
||||
curl -X POST "http://127.0.0.1:2468/v2/rpc" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "x-acp-connection-id: acp_conn_1" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","id":5,"method":"_sandboxagent/fs/delete_entry","params":{"path":"./notes-old.txt","sessionId":"my-session"}}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -148,15 +174,14 @@ Batch upload accepts `application/x-tar` only and extracts into the destination
|
|||
|
||||
<CodeGroup>
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import tar from "tar";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
const client = new SandboxAgentClient({ baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock" });
|
||||
|
||||
const archivePath = path.join(process.cwd(), "skills.tar");
|
||||
await tar.c({
|
||||
|
|
@ -176,7 +201,7 @@ console.log(result);
|
|||
```bash cURL
|
||||
tar -cf skills.tar -C ./skills .
|
||||
|
||||
curl -X POST "http://127.0.0.1:2468/v1/fs/upload-batch?path=./skills&sessionId=my-session" \
|
||||
curl -X POST "http://127.0.0.1:2468/v2/fs/upload-batch?path=./skills&sessionId=my-session" \
|
||||
-H "Authorization: Bearer $SANDBOX_TOKEN" \
|
||||
-H "Content-Type: application/x-tar" \
|
||||
--data-binary @skills.tar
|
||||
|
|
|
|||
|
|
@ -25,10 +25,11 @@ Two ways to receive events: SSE streaming (recommended) or polling.
|
|||
Use SSE for real-time events with automatic reconnection support.
|
||||
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
// Get offset from last stored event (0 returns all events)
|
||||
|
|
@ -130,7 +131,10 @@ const codingSession = actor({
|
|||
},
|
||||
|
||||
createVars: async (c): Promise<CodingSessionVars> => {
|
||||
const client = await SandboxAgent.connect({ baseUrl: c.state.baseUrl });
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: c.state.baseUrl,
|
||||
agent: "mock",
|
||||
});
|
||||
await client.createSession(c.state.sessionId, { agent: "claude" });
|
||||
return { client };
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ The `mcp` field is a map of server name to config. Use `type: "local"` for stdio
|
|||
<CodeGroup>
|
||||
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.createSession("claude-mcp", {
|
||||
agent: "claude",
|
||||
|
|
|
|||
2189
docs/openapi.json
2189
docs/openapi.json
File diff suppressed because it is too large
Load diff
|
|
@ -1,149 +1,26 @@
|
|||
---
|
||||
title: "OpenCode Compatibility"
|
||||
description: "Connect OpenCode clients, SDKs, and web UI to Sandbox Agent."
|
||||
description: "Status of the OpenCode bridge during ACP v2 migration."
|
||||
---
|
||||
|
||||
<Warning>
|
||||
**Experimental**: OpenCode SDK & UI support is experimental and may change without notice.
|
||||
</Warning>
|
||||
OpenCode compatibility is intentionally deferred during ACP core migration.
|
||||
|
||||
Sandbox Agent exposes an OpenCode-compatible API, allowing you to connect any OpenCode client, SDK, or web UI to control coding agents running inside sandboxes.
|
||||
## Current status (v2 core phases)
|
||||
|
||||
## Why Use OpenCode Clients with Sandbox Agent?
|
||||
- `/opencode/*` routes are disabled.
|
||||
- `sandbox-agent opencode` returns an explicit disabled error.
|
||||
- This is expected while ACP runtime, SDK, and inspector migration is completed.
|
||||
|
||||
OpenCode provides a rich ecosystem of clients:
|
||||
## Planned re-enable step
|
||||
|
||||
- **OpenCode CLI** (`opencode attach`): Terminal-based interface
|
||||
- **OpenCode Web UI**: Browser-based chat interface
|
||||
- **OpenCode SDK** (`@opencode-ai/sdk`): Rich TypeScript SDK
|
||||
OpenCode support is restored in a dedicated phase after ACP core is stable:
|
||||
|
||||
## Quick Start
|
||||
1. Reintroduce `/opencode/*` routing on top of ACP internals.
|
||||
2. Add dedicated OpenCode ↔ ACP integration tests.
|
||||
3. Re-enable OpenCode docs and operational guidance.
|
||||
|
||||
### Using OpenCode CLI & TUI
|
||||
Track details in:
|
||||
|
||||
Sandbox Agent provides an all-in-one command to setup Sandbox Agent and connect an OpenCode session, great for local development:
|
||||
|
||||
```bash
|
||||
sandbox-agent opencode --port 2468 --no-token
|
||||
```
|
||||
|
||||
Or, start the server and attach separately:
|
||||
|
||||
```bash
|
||||
# Start sandbox-agent
|
||||
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
|
||||
|
||||
# Attach OpenCode CLI
|
||||
opencode attach http://localhost:2468/opencode
|
||||
```
|
||||
|
||||
With authentication enabled:
|
||||
|
||||
```bash
|
||||
# Start with token
|
||||
sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||
|
||||
# Attach with password
|
||||
opencode attach http://localhost:2468/opencode --password "$SANDBOX_TOKEN"
|
||||
```
|
||||
|
||||
### Using the OpenCode Web UI
|
||||
|
||||
The OpenCode web UI can connect to Sandbox Agent for a full browser-based experience.
|
||||
|
||||
<Steps>
|
||||
<Step title="Start Sandbox Agent with CORS">
|
||||
```bash
|
||||
sandbox-agent server --no-token --host 127.0.0.1 --port 2468 --cors-allow-origin http://127.0.0.1:5173
|
||||
```
|
||||
</Step>
|
||||
<Step title="Clone and Start the OpenCode Web App">
|
||||
```bash
|
||||
git clone https://github.com/anomalyco/opencode
|
||||
cd opencode/packages/app
|
||||
export VITE_OPENCODE_SERVER_HOST=127.0.0.1
|
||||
export VITE_OPENCODE_SERVER_PORT=2468
|
||||
bun install
|
||||
bun run dev -- --host 127.0.0.1 --port 5173
|
||||
```
|
||||
</Step>
|
||||
<Step title="Open the UI">
|
||||
Navigate to `http://127.0.0.1:5173/` in your browser.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
If you see `Error: Could not connect to server`, check that:
|
||||
- The sandbox-agent server is running
|
||||
- `--cors-allow-origin` matches the **exact** browser origin (`localhost` and `127.0.0.1` are different origins)
|
||||
</Note>
|
||||
|
||||
### Using OpenCode SDK
|
||||
|
||||
```typescript
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk";
|
||||
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: "http://localhost:2468/opencode",
|
||||
headers: { Authorization: "Bearer YOUR_TOKEN" }, // if using auth
|
||||
});
|
||||
|
||||
// Create a session
|
||||
const session = await client.session.create();
|
||||
|
||||
// Send a prompt
|
||||
await client.session.promptAsync({
|
||||
path: { id: session.data.id },
|
||||
body: {
|
||||
parts: [{ type: "text", text: "Hello, write a hello world script" }],
|
||||
},
|
||||
});
|
||||
|
||||
// Subscribe to events
|
||||
const events = await client.event.subscribe({});
|
||||
for await (const event of events.stream) {
|
||||
console.log(event);
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- **API Routing**: The OpenCode API is available at the `/opencode` base path
|
||||
- **Authentication**: If sandbox-agent is started with `--token`, include `Authorization: Bearer <token>` header or use `--password` flag with CLI
|
||||
- **CORS**: When using the web UI from a different origin, configure `--cors-allow-origin`
|
||||
- **Provider Selection**: Use the provider/model selector in the UI to choose which backing agent to use (claude, codex, opencode, amp)
|
||||
- **Models & Variants**: Providers are grouped by backing agent (e.g. Claude Code, Codex, Amp). OpenCode models are grouped by `OpenCode (<provider>)` to preserve their native provider grouping. Each model keeps its real model ID, and variants are exposed when available (Codex/OpenCode/Amp).
|
||||
- **Optional Native Proxy for TUI/Config Endpoints**: Set `OPENCODE_COMPAT_PROXY_URL` (for example `http://127.0.0.1:4096`) to proxy select OpenCode-native endpoints to a real OpenCode server. This currently applies to `/command`, `/config`, `/global/config`, and `/tui/*`. If not set, sandbox-agent uses its built-in compatibility handlers.
|
||||
|
||||
## Endpoint Coverage
|
||||
|
||||
See the full endpoint compatibility table below. Most endpoints are functional for session management, messaging, and event streaming. Some endpoints return stub responses for features not yet implemented.
|
||||
|
||||
<Accordion title="Endpoint Status Table">
|
||||
|
||||
| Endpoint | Status | Notes |
|
||||
|---|---|---|
|
||||
| `GET /event` | ✓ | Emits events for session/message updates (SSE) |
|
||||
| `GET /global/event` | ✓ | Wraps events in GlobalEvent format (SSE) |
|
||||
| `GET /session` | ✓ | In-memory session store |
|
||||
| `POST /session` | ✓ | Create new sessions |
|
||||
| `GET /session/{id}` | ✓ | Get session details |
|
||||
| `POST /session/{id}/message` | ✓ | Send messages to session |
|
||||
| `GET /session/{id}/message` | ✓ | Get session messages |
|
||||
| `GET /permission` | ✓ | List pending permissions |
|
||||
| `POST /permission/{id}/reply` | ✓ | Respond to permission requests |
|
||||
| `GET /question` | ✓ | List pending questions |
|
||||
| `POST /question/{id}/reply` | ✓ | Answer agent questions |
|
||||
| `GET /provider` | ✓ | Returns provider metadata |
|
||||
| `GET /command` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise stub response |
|
||||
| `GET /config` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise stub response |
|
||||
| `PATCH /config` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise local compatibility behavior |
|
||||
| `GET /global/config` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise stub response |
|
||||
| `PATCH /global/config` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise local compatibility behavior |
|
||||
| `/tui/*` | ↔ | Proxied to native OpenCode when `OPENCODE_COMPAT_PROXY_URL` is set; otherwise local compatibility behavior |
|
||||
| `GET /agent` | − | Returns agent list |
|
||||
| *other endpoints* | − | Return empty/stub responses |
|
||||
|
||||
✓ Functional ↔ Proxied (optional) − Stubbed
|
||||
|
||||
</Accordion>
|
||||
- `research/acp/spec.md`
|
||||
- `research/acp/migration-steps.md`
|
||||
- `research/acp/todo.md`
|
||||
|
|
|
|||
|
|
@ -184,9 +184,12 @@ icon: "rocket"
|
|||
<Tabs>
|
||||
<Tab title="TypeScript">
|
||||
```typescript
|
||||
const client = await SandboxAgent.connect({
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://your-server:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
agent: "mock",
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
|
|
@ -230,10 +233,11 @@ icon: "rocket"
|
|||
<Tabs>
|
||||
<Tab title="TypeScript">
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
agent: "claude",
|
||||
});
|
||||
|
||||
await client.createSession("my-session", {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
---
|
||||
title: "TypeScript"
|
||||
description: "Use the generated client to manage sessions and stream events."
|
||||
description: "Use the TypeScript SDK to manage ACP sessions and Sandbox Agent HTTP APIs."
|
||||
icon: "js"
|
||||
---
|
||||
|
||||
The TypeScript SDK is generated from the OpenAPI spec that ships with the server. It provides a typed
|
||||
client for sessions, events, and agent operations.
|
||||
The TypeScript SDK is centered on `sandbox-agent` and its `SandboxAgentClient`, which provides a Sandbox-facing API for session flows, ACP extensions, and binary HTTP filesystem helpers.
|
||||
|
||||
## Install
|
||||
|
||||
|
|
@ -27,14 +26,17 @@ client for sessions, events, and agent operations.
|
|||
## Create a client
|
||||
|
||||
```ts
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
agent: "mock",
|
||||
});
|
||||
```
|
||||
|
||||
`SandboxAgentClient` is the canonical API. By default it auto-connects (`autoConnect: true`), so provide `agent` in the constructor. Use the instance method `client.connect()` only when you explicitly set `autoConnect: false`.
|
||||
|
||||
## Autospawn (Node only)
|
||||
|
||||
If you run locally, the SDK can launch the server for you.
|
||||
|
|
@ -42,7 +44,9 @@ If you run locally, the SDK can launch the server for you.
|
|||
```ts
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.start();
|
||||
const client = await SandboxAgent.start({
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.dispose();
|
||||
```
|
||||
|
|
@ -50,69 +54,162 @@ await client.dispose();
|
|||
Autospawn uses the local `sandbox-agent` binary. Install `@sandbox-agent/cli` (recommended) or set
|
||||
`SANDBOX_AGENT_BIN` to a custom path.
|
||||
|
||||
## Sessions and messages
|
||||
## Connect lifecycle
|
||||
|
||||
Use manual mode when you want explicit ACP session lifecycle control.
|
||||
|
||||
```ts
|
||||
await client.createSession("demo-session", {
|
||||
agent: "codex",
|
||||
agentMode: "default",
|
||||
permissionMode: "plan",
|
||||
import {
|
||||
AlreadyConnectedError,
|
||||
NotConnectedError,
|
||||
SandboxAgentClient,
|
||||
} from "sandbox-agent";
|
||||
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
agent: "mock",
|
||||
autoConnect: false,
|
||||
});
|
||||
|
||||
await client.postMessage("demo-session", { message: "Hello" });
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
} catch (error) {
|
||||
if (error instanceof AlreadyConnectedError) {
|
||||
console.error("already connected");
|
||||
}
|
||||
}
|
||||
|
||||
await client.disconnect();
|
||||
|
||||
try {
|
||||
await client.prompt({ sessionId: "s", prompt: [{ type: "text", text: "hi" }] });
|
||||
} catch (error) {
|
||||
if (error instanceof NotConnectedError) {
|
||||
console.error("connect first");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
List agents and inspect feature coverage (available on `capabilities`):
|
||||
## Session flow
|
||||
|
||||
```ts
|
||||
const session = await client.newSession({
|
||||
cwd: "/",
|
||||
mcpServers: [],
|
||||
metadata: {
|
||||
agent: "mock",
|
||||
title: "Demo Session",
|
||||
variant: "high",
|
||||
permissionMode: "ask",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await client.prompt({
|
||||
sessionId: session.sessionId,
|
||||
prompt: [{ type: "text", text: "Summarize this repository." }],
|
||||
});
|
||||
|
||||
console.log(result.stopReason);
|
||||
```
|
||||
|
||||
Load, cancel, and runtime settings use ACP-aligned method names:
|
||||
|
||||
```ts
|
||||
await client.loadSession({ sessionId: session.sessionId, cwd: "/", mcpServers: [] });
|
||||
await client.cancel({ sessionId: session.sessionId });
|
||||
await client.setSessionMode({ sessionId: session.sessionId, modeId: "default" });
|
||||
await client.setSessionConfigOption({
|
||||
sessionId: session.sessionId,
|
||||
configId: "config-id-from-session",
|
||||
value: "config-value-id",
|
||||
});
|
||||
```
|
||||
|
||||
## Extension helpers
|
||||
|
||||
Sandbox extensions are exposed as first-class methods:
|
||||
|
||||
```ts
|
||||
const models = await client.listModels({ sessionId: session.sessionId });
|
||||
console.log(models.currentModelId, models.availableModels.length);
|
||||
|
||||
await client.setMetadata(session.sessionId, {
|
||||
title: "Renamed Session",
|
||||
model: "mock",
|
||||
permissionMode: "ask",
|
||||
});
|
||||
|
||||
await client.detachSession(session.sessionId);
|
||||
await client.terminateSession(session.sessionId);
|
||||
```
|
||||
|
||||
## Event handling
|
||||
|
||||
Use `onEvent` to consume converted SDK events.
|
||||
|
||||
```ts
|
||||
import { SandboxAgentClient, type AgentEvent } from "sandbox-agent";
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
agent: "mock",
|
||||
onEvent: (event) => {
|
||||
events.push(event);
|
||||
|
||||
if (event.type === "sessionEnded") {
|
||||
console.log("ended", event.notification.params.sessionId ?? event.notification.params.session_id);
|
||||
}
|
||||
|
||||
if (event.type === "agentUnparsed") {
|
||||
console.warn("unparsed", event.notification.params);
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also handle raw session update notifications directly:
|
||||
|
||||
```ts
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
agent: "mock",
|
||||
onSessionUpdate: (notification) => {
|
||||
console.log(notification.update.sessionUpdate);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Control + HTTP helpers
|
||||
|
||||
Agent/session and non-binary filesystem control helpers use ACP extension methods over `/v2/rpc`:
|
||||
|
||||
```ts
|
||||
const health = await client.getHealth();
|
||||
const agents = await client.listAgents();
|
||||
const codex = agents.agents.find((agent) => agent.id === "codex");
|
||||
console.log(codex?.capabilities);
|
||||
await client.installAgent("codex", { reinstall: true });
|
||||
|
||||
const sessions = await client.listSessions();
|
||||
const sessionInfo = await client.getSession(sessions.sessions[0].session_id);
|
||||
```
|
||||
|
||||
## Poll events
|
||||
These methods require an active ACP connection and throw `NotConnectedError` when disconnected.
|
||||
|
||||
```ts
|
||||
const events = await client.getEvents("demo-session", {
|
||||
offset: 0,
|
||||
limit: 200,
|
||||
includeRaw: false,
|
||||
});
|
||||
Binary filesystem transfer intentionally remains HTTP:
|
||||
|
||||
for (const event of events.events) {
|
||||
console.log(event.type, event.data);
|
||||
}
|
||||
```
|
||||
- `readFsFile` -> `GET /v2/fs/file`
|
||||
- `writeFsFile` -> `PUT /v2/fs/file`
|
||||
- `uploadFsBatch` -> `POST /v2/fs/upload-batch`
|
||||
|
||||
## Stream events (SSE)
|
||||
Reason: these are Sandbox Agent host/runtime filesystem operations (not agent-specific ACP behavior), intentionally separate from ACP native `fs/read_text_file` / `fs/write_text_file`, and they may require streaming very large binary payloads that ACP JSON-RPC is not suited to transport efficiently.
|
||||
|
||||
```ts
|
||||
for await (const event of client.streamEvents("demo-session", {
|
||||
offset: 0,
|
||||
includeRaw: false,
|
||||
})) {
|
||||
console.log(event.type, event.data);
|
||||
}
|
||||
```
|
||||
|
||||
The SDK parses `text/event-stream` into `UniversalEvent` objects. If you want full control, use
|
||||
`getEventsSse()` and parse the stream yourself.
|
||||
|
||||
## Stream a single turn
|
||||
|
||||
```ts
|
||||
for await (const event of client.streamTurn("demo-session", { message: "Hello" })) {
|
||||
console.log(event.type, event.data);
|
||||
}
|
||||
```
|
||||
|
||||
This method posts the message and streams only the next turn. For manual control, call
|
||||
`postMessageStream()` and parse the SSE response yourself.
|
||||
|
||||
## Optional raw payloads
|
||||
|
||||
Set `includeRaw: true` on `getEvents`, `streamEvents`, or `streamTurn` to include the raw provider
|
||||
payload in `event.raw`. This is useful for debugging and conversion analysis.
|
||||
ACP extension variants can exist in parallel for compatibility, but `SandboxAgentClient` should prefer the HTTP endpoints above by default.
|
||||
|
||||
## Error handling
|
||||
|
||||
|
|
@ -122,7 +219,7 @@ All HTTP errors throw `SandboxAgentError`:
|
|||
import { SandboxAgentError } from "sandbox-agent";
|
||||
|
||||
try {
|
||||
await client.postMessage("missing-session", { message: "Hi" });
|
||||
await client.listAgents();
|
||||
} catch (error) {
|
||||
if (error instanceof SandboxAgentError) {
|
||||
console.error(error.status, error.problem);
|
||||
|
|
@ -142,6 +239,7 @@ const url = buildInspectorUrl({
|
|||
token: "optional-bearer-token",
|
||||
headers: { "X-Custom-Header": "value" },
|
||||
});
|
||||
|
||||
console.log(url);
|
||||
// https://your-sandbox-agent.example.com/ui/?token=...&headers=...
|
||||
```
|
||||
|
|
@ -153,10 +251,17 @@ Parameters:
|
|||
|
||||
## Types
|
||||
|
||||
The SDK exports OpenAPI-derived types for events, items, and feature coverage:
|
||||
The SDK exports typed events and responses for the Sandbox layer:
|
||||
|
||||
```ts
|
||||
import type { UniversalEvent, UniversalItem, AgentCapabilities } from "sandbox-agent";
|
||||
import type {
|
||||
AgentEvent,
|
||||
AgentInfo,
|
||||
HealthResponse,
|
||||
SessionInfo,
|
||||
SessionListResponse,
|
||||
SessionTerminateResponse,
|
||||
} from "sandbox-agent";
|
||||
```
|
||||
|
||||
See the [API Reference](/api) for schema details.
|
||||
For low-level protocol transport details, see [ACP HTTP Client](/advanced/acp-http-client).
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ Pass `skills.sources` when creating a session to load skills from GitHub repos,
|
|||
<CodeGroup>
|
||||
|
||||
```ts TypeScript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { SandboxAgentClient } from "sandbox-agent";
|
||||
|
||||
const client = await SandboxAgent.connect({
|
||||
const client = new SandboxAgentClient({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
token: process.env.SANDBOX_TOKEN,
|
||||
});
|
||||
agent: "mock",
|
||||
});
|
||||
|
||||
await client.createSession("claude-skills", {
|
||||
agent: "claude",
|
||||
|
|
|
|||
|
|
@ -6,12 +6,17 @@
|
|||
- Do not bind mount host files or host directories into Docker example containers.
|
||||
- If an example needs tools, skills, or MCP servers, install them inside the container during setup.
|
||||
|
||||
## Testing Examples
|
||||
## Testing Examples (ACP v2)
|
||||
|
||||
Examples can be tested by starting them in the background and communicating directly with the sandbox-agent API:
|
||||
Examples should be validated against v2 endpoints:
|
||||
|
||||
1. Start the example: `SANDBOX_AGENT_DEV=1 pnpm start &`
|
||||
2. Note the base URL and session ID from the output.
|
||||
3. Send messages: `curl -X POST http://127.0.0.1:<port>/v1/sessions/<sessionId>/messages -H "Content-Type: application/json" -d '{"message":"..."}'`
|
||||
4. Poll events: `curl http://127.0.0.1:<port>/v1/sessions/<sessionId>/events`
|
||||
5. Approve permissions: `curl -X POST http://127.0.0.1:<port>/v1/sessions/<sessionId>/permissions/<permissionId>/reply -H "Content-Type: application/json" -d '{"reply":"once"}'`
|
||||
1. Start the example: `SANDBOX_AGENT_DEV=1 pnpm start`
|
||||
2. Create an ACP client by POSTing `initialize` to `/v2/rpc` with `x-acp-agent: mock` (or another installed agent).
|
||||
3. Capture `x-acp-connection-id` from the response headers.
|
||||
4. Open SSE stream: `GET /v2/rpc` with `x-acp-connection-id`.
|
||||
5. Send `session/new` then `session/prompt` via `POST /v2/rpc` with the same connection id.
|
||||
6. Close connection via `DELETE /v2/rpc` with `x-acp-connection-id`.
|
||||
|
||||
v1 reminder:
|
||||
|
||||
- `/v1/*` is removed and returns `410 Gone`.
|
||||
|
|
|
|||
|
|
@ -1,150 +1,26 @@
|
|||
## Frontend Style Guide
|
||||
# Frontend Instructions
|
||||
|
||||
Examples should follow these design conventions:
|
||||
## Inspector Architecture
|
||||
|
||||
**Color Palette (Dark Theme)**
|
||||
- Primary accent: `#ff4f00` (orange) for interactive elements and highlights
|
||||
- Background: `#000000` (main), `#1c1c1e` (cards/containers)
|
||||
- Borders: `#2c2c2e`
|
||||
- Input backgrounds: `#2c2c2e` with border `#3a3a3c`
|
||||
- Text: `#ffffff` (primary), `#8e8e93` (secondary/muted)
|
||||
- Success: `#30d158` (green)
|
||||
- Warning: `#ff4f00` (orange)
|
||||
- Danger: `#ff3b30` (red)
|
||||
- Purple: `#bf5af2` (for special states like rollback)
|
||||
- Inspector source is `frontend/packages/inspector/`.
|
||||
- `/ui/` must use ACP over HTTP (`/v2/rpc`) for session/prompt traffic.
|
||||
- Primary flow:
|
||||
- `initialize`
|
||||
- `session/new`
|
||||
- `session/prompt`
|
||||
- `session/update` over SSE
|
||||
- Keep backend/protocol changes in client bindings; avoid unnecessary full UI rewrites.
|
||||
|
||||
**Typography**
|
||||
- UI: System fonts (`-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif`)
|
||||
- Code: `ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace`
|
||||
- Sizes: 14-16px body, 12-13px labels, large numbers 48-72px
|
||||
## Testing
|
||||
|
||||
**Sizing & Spacing**
|
||||
- Border radius: 8px (cards/containers/buttons), 6px (inputs/badges)
|
||||
- Section padding: 20-24px
|
||||
- Gap between items: 12px
|
||||
- Transitions: 200ms ease for all interactive states
|
||||
Run inspector checks after transport or chat-flow changes:
|
||||
```bash
|
||||
pnpm --filter @sandbox-agent/inspector test
|
||||
pnpm --filter @sandbox-agent/inspector test:agent-browser
|
||||
```
|
||||
|
||||
**Button Styles**
|
||||
- Padding: 12px 20px
|
||||
- Border: none
|
||||
- Border radius: 8px
|
||||
- Font size: 14px, weight 600
|
||||
- Hover: none (no hover state)
|
||||
- Disabled: 50% opacity, `cursor: not-allowed`
|
||||
|
||||
**CSS Approach**
|
||||
- Plain CSS in `<style>` tag within index.html (no preprocessors or Tailwind)
|
||||
- Class-based selectors with state modifiers (`.active`, `.complete`, `.running`)
|
||||
- Focus states use primary accent color (`#ff4f00`) for borders with subtle box-shadow
|
||||
|
||||
**Spacing System**
|
||||
- Base unit: 4px
|
||||
- Scale: 4px, 8px, 12px, 16px, 20px, 24px, 32px, 48px
|
||||
- Component internal padding: 12-16px
|
||||
- Section/card padding: 20px
|
||||
- Card header padding: 16px 20px
|
||||
- Gap between related items: 8-12px
|
||||
- Gap between sections: 24-32px
|
||||
- Margin between major blocks: 32px
|
||||
|
||||
**Iconography**
|
||||
- Icon library: [Lucide](https://lucide.dev/) (React: `lucide-react`)
|
||||
- Standard sizes: 16px (inline/small), 20px (buttons/UI), 24px (standalone/headers)
|
||||
- Icon color: inherit from parent text color, or use `currentColor`
|
||||
- Icon-only buttons must include `aria-label` for accessibility
|
||||
- Stroke width: 2px (default), 1.5px for smaller icons
|
||||
|
||||
**Component Patterns**
|
||||
|
||||
*Buttons*
|
||||
- Primary: `#ff4f00` background, white text
|
||||
- Secondary: `#2c2c2e` background, white text
|
||||
- Ghost: transparent background, `#ff4f00` text
|
||||
- Danger: `#ff3b30` background, white text
|
||||
- Success: `#30d158` background, white text
|
||||
- Disabled: 50% opacity, `cursor: not-allowed`
|
||||
|
||||
*Form Inputs*
|
||||
- Background: `#2c2c2e`
|
||||
- Border: 1px solid `#3a3a3c`
|
||||
- Border radius: 8px
|
||||
- Padding: 12px 16px
|
||||
- Focus: border-color `#ff4f00`, box-shadow `0 0 0 3px rgba(255, 79, 0, 0.2)`
|
||||
- Placeholder text: `#6e6e73`
|
||||
|
||||
*Cards/Containers*
|
||||
- Background: `#1c1c1e`
|
||||
- Border: 1px solid `#2c2c2e`
|
||||
- Border radius: 8px
|
||||
- Padding: 20px
|
||||
- Box shadow: `0 1px 3px rgba(0, 0, 0, 0.3)`
|
||||
- Header style (when applicable):
|
||||
- Background: `#2c2c2e`
|
||||
- Padding: 16px 20px
|
||||
- Font size: 18px, weight 600
|
||||
- Border bottom: 1px solid `#2c2c2e`
|
||||
- Border radius: 8px 8px 0 0 (top corners only)
|
||||
- Negative margin to align with card edges: `-20px -20px 20px -20px`
|
||||
|
||||
*Modals/Overlays*
|
||||
- Backdrop: `rgba(0, 0, 0, 0.75)`
|
||||
- Modal background: `#1c1c1e`
|
||||
- Border radius: 8px
|
||||
- Max-width: 480px (small), 640px (medium), 800px (large)
|
||||
- Padding: 24px
|
||||
- Close button: top-right, 8px from edges
|
||||
|
||||
*Lists*
|
||||
- Item padding: 12px 16px
|
||||
- Dividers: 1px solid `#2c2c2e`
|
||||
- Hover background: `#2c2c2e`
|
||||
- Selected/active background: `rgba(255, 79, 0, 0.15)`
|
||||
|
||||
*Badges/Tags*
|
||||
- Padding: 4px 8px
|
||||
- Border radius: 6px
|
||||
- Font size: 12px
|
||||
- Font weight: 500
|
||||
|
||||
*Tabs*
|
||||
- Container: `border-bottom: 1px solid #2c2c2e`, flex-wrap for overflow
|
||||
- Tab: `padding: 12px 16px`, no background, `border-radius: 0`
|
||||
- Tab border: `border-bottom: 2px solid transparent`, `margin-bottom: -1px`
|
||||
- Tab text: `#8e8e93` (muted), font-weight 600, font-size 14px
|
||||
- Active tab: `color: #ffffff`, `border-bottom-color: #ff4f00`
|
||||
- Hover: none (no hover state)
|
||||
- Transition: `color 200ms ease, border-color 200ms ease`
|
||||
|
||||
**UI States**
|
||||
|
||||
*Loading States*
|
||||
- Spinner: 20px for inline, 32px for page-level
|
||||
- Skeleton placeholders: `#2c2c2e` background with subtle pulse animation
|
||||
- Loading text: "Loading..." in muted color
|
||||
- Button loading: show spinner, disable interaction, keep button width stable
|
||||
|
||||
*Empty States*
|
||||
- Center content vertically and horizontally
|
||||
- Icon: 48px, muted color (`#6e6e73`)
|
||||
- Heading: 18px, primary text color
|
||||
- Description: 14px, muted color
|
||||
- Optional action button below description
|
||||
|
||||
*Error States*
|
||||
- Inline errors: `#ff3b30` text below input, 12px font size
|
||||
- Error banners: `#ff3b30` left border (4px), `rgba(255, 59, 48, 0.1)` background
|
||||
- Form validation: highlight input border in `#ff3b30`
|
||||
- Error icon: Lucide `AlertCircle` or `XCircle`
|
||||
|
||||
*Disabled States*
|
||||
- Opacity: 50%
|
||||
- Cursor: `not-allowed`
|
||||
- No hover/focus effects
|
||||
- Preserve layout (don't collapse or hide)
|
||||
|
||||
*Success States*
|
||||
- Color: `#30d158`
|
||||
- Icon: Lucide `CheckCircle` or `Check`
|
||||
- Toast/banner: `rgba(48, 209, 88, 0.1)` background with green left border
|
||||
## Docs Sync
|
||||
|
||||
- Update `docs/inspector.mdx` when `/ui/` behavior changes.
|
||||
- Update `docs/sdks/typescript.mdx` when inspector SDK bindings or ACP transport behavior changes.
|
||||
|
||||
|
|
|
|||
|
|
@ -1168,26 +1168,6 @@
|
|||
width: auto;
|
||||
}
|
||||
|
||||
.mock-agent-hint {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 320px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mock-agent-hint code {
|
||||
background: var(--border-2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.empty-state-menu-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ArrowLeft, ArrowRight, ChevronDown, ChevronRight, Pencil, Plus, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SkillSource } from "sandbox-agent";
|
||||
import type { McpServerEntry } from "../App";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SkillSource } from "../types/legacyApi";
|
||||
|
||||
export type SessionConfig = {
|
||||
model: string;
|
||||
|
|
@ -14,8 +14,7 @@ const agentLabels: Record<string, string> = {
|
|||
claude: "Claude Code",
|
||||
codex: "Codex",
|
||||
opencode: "OpenCode",
|
||||
amp: "Amp",
|
||||
mock: "Mock"
|
||||
amp: "Amp"
|
||||
};
|
||||
|
||||
const validateServerJson = (json: string): string | null => {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const badges = [
|
|||
type BadgeItem = (typeof badges)[number];
|
||||
|
||||
const getEnabled = (featureCoverage: FeatureCoverageView, key: BadgeItem["key"]) =>
|
||||
Boolean((featureCoverage as Record<string, boolean | undefined>)[key]);
|
||||
Boolean((featureCoverage as unknown as Record<string, boolean | undefined>)[key]);
|
||||
|
||||
const FeatureCoverageBadges = ({ featureCoverage }: { featureCoverage: FeatureCoverageView }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ const ChatMessages = ({
|
|||
const isInProgress = item.status === "in_progress";
|
||||
const isFailed = item.status === "failed";
|
||||
const messageClass = getMessageClass(item);
|
||||
const statusLabel = item.status !== "completed" ? item.status.replace("_", " ") : "";
|
||||
const statusValue = item.status ?? "";
|
||||
const statusLabel =
|
||||
statusValue && statusValue !== "completed" ? statusValue.replace("_", " ") : "";
|
||||
const kindLabel = item.kind.replace("_", " ");
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { MessageSquare, Plus, Square, Terminal } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, PermissionEventData, QuestionEventData, SkillSource } from "sandbox-agent";
|
||||
import type { McpServerEntry } from "../../App";
|
||||
import type {
|
||||
AgentInfo,
|
||||
AgentModelInfo,
|
||||
AgentModeInfo,
|
||||
PermissionEventData,
|
||||
QuestionEventData,
|
||||
SkillSource
|
||||
} from "../../types/legacyApi";
|
||||
import ApprovalsTab from "../debug/ApprovalsTab";
|
||||
import SessionCreateMenu, { type SessionConfig } from "../SessionCreateMenu";
|
||||
import ChatInput from "./ChatInput";
|
||||
|
|
@ -175,11 +182,6 @@ const ChatPanel = ({
|
|||
<Terminal className="empty-state-icon" />
|
||||
<div className="empty-state-title">Ready to Chat</div>
|
||||
<p className="empty-state-text">Send a message to start a conversation with the agent.</p>
|
||||
{agentLabel === "Mock" && (
|
||||
<div className="mock-agent-hint">
|
||||
The mock agent simulates agent responses for testing the inspector UI without requiring API credentials. Send <code>help</code> for available commands.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ChatMessages
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { UniversalItem } from "sandbox-agent";
|
||||
import type { UniversalItem } from "../../types/legacyApi";
|
||||
|
||||
export const getMessageClass = (item: UniversalItem) => {
|
||||
if (item.kind === "tool_call" || item.kind === "tool_result") return "tool";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ContentPart } from "sandbox-agent";
|
||||
import type { ContentPart } from "../../types/legacyApi";
|
||||
import { formatJson } from "../../utils/format";
|
||||
|
||||
const renderContentPart = (part: ContentPart, index: number) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { UniversalItem } from "sandbox-agent";
|
||||
import type { UniversalItem } from "../../types/legacyApi";
|
||||
|
||||
export type TimelineEntry = {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Download, Loader2, RefreshCw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { AgentInfo, AgentModeInfo } from "sandbox-agent";
|
||||
import type { AgentInfo, AgentModeInfo } from "../../types/legacyApi";
|
||||
import FeatureCoverageBadges from "../agents/FeatureCoverageBadges";
|
||||
import { emptyFeatureCoverage } from "../../types/agents";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { HelpCircle, Shield } from "lucide-react";
|
||||
import type { PermissionEventData, QuestionEventData } from "sandbox-agent";
|
||||
import type { PermissionEventData, QuestionEventData } from "../../types/legacyApi";
|
||||
import { formatJson } from "../../utils/format";
|
||||
|
||||
const ApprovalsTab = ({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Cloud, PlayCircle, Terminal } from "lucide-react";
|
||||
import type { AgentInfo, AgentModeInfo, UniversalEvent } from "sandbox-agent";
|
||||
import type { AgentInfo, AgentModeInfo, UniversalEvent } from "../../types/legacyApi";
|
||||
import AgentsTab from "./AgentsTab";
|
||||
import EventsTab from "./EventsTab";
|
||||
import RequestLogTab from "./RequestLogTab";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { UniversalEvent } from "sandbox-agent";
|
||||
import type { UniversalEvent } from "../../types/legacyApi";
|
||||
import { formatJson, formatTime } from "../../utils/format";
|
||||
import { getEventCategory, getEventClass, getEventIcon, getEventKey, getEventType } from "./eventUtils";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Clipboard } from "lucide-react";
|
||||
import { ChevronDown, ChevronRight, Clipboard } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { RequestLog } from "../../types/requestLog";
|
||||
import { formatJson } from "../../utils/format";
|
||||
|
||||
const RequestLogTab = ({
|
||||
requestLog,
|
||||
|
|
@ -13,6 +15,12 @@ const RequestLogTab = ({
|
|||
onClear: () => void;
|
||||
onCopy: (entry: RequestLog) => void;
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState<Record<number, boolean>>({});
|
||||
|
||||
const toggleExpanded = (id: number) => {
|
||||
setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
|
||||
|
|
@ -25,28 +33,94 @@ const RequestLogTab = ({
|
|||
{requestLog.length === 0 ? (
|
||||
<div className="card-meta">No requests logged yet.</div>
|
||||
) : (
|
||||
requestLog.map((entry) => (
|
||||
<div key={entry.id} className="log-item">
|
||||
<span className="log-method">{entry.method}</span>
|
||||
<span className="log-url text-truncate">{entry.url}</span>
|
||||
<span className={`log-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>
|
||||
{entry.status || "ERR"}
|
||||
</span>
|
||||
<div className="log-meta">
|
||||
<span>
|
||||
{entry.time}
|
||||
{entry.error && ` - ${entry.error}`}
|
||||
</span>
|
||||
<button className="copy-button" onClick={() => onCopy(entry)}>
|
||||
<Clipboard />
|
||||
{copiedLogId === entry.id ? "Copied" : "curl"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
<div className="event-list">
|
||||
{requestLog.map((entry) => {
|
||||
const isExpanded = expanded[entry.id] ?? false;
|
||||
const hasDetails = entry.headers || entry.body || entry.responseBody;
|
||||
return (
|
||||
<div key={entry.id} className={`event-item ${isExpanded ? "expanded" : "collapsed"}`}>
|
||||
<button
|
||||
className="event-summary"
|
||||
type="button"
|
||||
onClick={() => hasDetails && toggleExpanded(entry.id)}
|
||||
title={hasDetails ? (isExpanded ? "Collapse" : "Expand") : undefined}
|
||||
style={{ cursor: hasDetails ? "pointer" : "default" }}
|
||||
>
|
||||
<div className="event-summary-main" style={{ flex: 1 }}>
|
||||
<div className="event-title-row">
|
||||
<span className="log-method">{entry.method}</span>
|
||||
<span className="log-url text-truncate" style={{ flex: 1 }}>{entry.url}</span>
|
||||
<span className={`log-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>
|
||||
{entry.status || "ERR"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="event-id">
|
||||
{entry.time}
|
||||
{entry.error && ` - ${entry.error}`}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="copy-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCopy(entry);
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
onCopy(entry);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clipboard size={14} />
|
||||
{copiedLogId === entry.id ? "Copied" : "curl"}
|
||||
</span>
|
||||
{hasDetails && (
|
||||
<span className="event-chevron">
|
||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="event-payload" style={{ padding: "8px 12px" }}>
|
||||
{entry.headers && Object.keys(entry.headers).length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div className="part-title">Request Headers</div>
|
||||
<pre className="code-block">{Object.entries(entry.headers).map(([k, v]) => `${k}: ${v}`).join("\n")}</pre>
|
||||
</div>
|
||||
)}
|
||||
{entry.body && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div className="part-title">Request Body</div>
|
||||
<pre className="code-block">{formatJsonSafe(entry.body)}</pre>
|
||||
</div>
|
||||
)}
|
||||
{entry.responseBody && (
|
||||
<div>
|
||||
<div className="part-title">Response Body</div>
|
||||
<pre className="code-block">{formatJsonSafe(entry.responseBody)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const formatJsonSafe = (text: string): string => {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
return formatJson(parsed);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
export default RequestLogTab;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
CheckCircle,
|
||||
FileDiff,
|
||||
HelpCircle,
|
||||
Info,
|
||||
MessageSquare,
|
||||
PauseCircle,
|
||||
PlayCircle,
|
||||
|
|
@ -13,7 +14,7 @@ import {
|
|||
Wrench,
|
||||
Zap
|
||||
} from "lucide-react";
|
||||
import type { UniversalEvent } from "sandbox-agent";
|
||||
import type { UniversalEvent } from "../../types/legacyApi";
|
||||
|
||||
export const getEventType = (event: UniversalEvent) => event.type;
|
||||
|
||||
|
|
@ -26,10 +27,45 @@ export const getEventClass = (type: string) => type.replace(/\./g, "-");
|
|||
|
||||
export const getEventIcon = (type: string) => {
|
||||
switch (type) {
|
||||
// ACP session update events
|
||||
case "acp.agent_message_chunk":
|
||||
return MessageSquare;
|
||||
case "acp.user_message_chunk":
|
||||
return MessageSquare;
|
||||
case "acp.agent_thought_chunk":
|
||||
return Brain;
|
||||
case "acp.tool_call":
|
||||
return Wrench;
|
||||
case "acp.tool_call_update":
|
||||
return Activity;
|
||||
case "acp.plan":
|
||||
return FileDiff;
|
||||
case "acp.session_info_update":
|
||||
return Info;
|
||||
case "acp.usage_update":
|
||||
return Info;
|
||||
case "acp.current_mode_update":
|
||||
return Info;
|
||||
case "acp.config_option_update":
|
||||
return Info;
|
||||
case "acp.available_commands_update":
|
||||
return Terminal;
|
||||
|
||||
// Inspector lifecycle events
|
||||
case "inspector.turn_started":
|
||||
return PlayCircle;
|
||||
case "inspector.turn_ended":
|
||||
return PauseCircle;
|
||||
case "inspector.user_message":
|
||||
return MessageSquare;
|
||||
|
||||
// Session lifecycle (inspector-emitted)
|
||||
case "session.started":
|
||||
return PlayCircle;
|
||||
case "session.ended":
|
||||
return PauseCircle;
|
||||
|
||||
// Legacy synthetic events
|
||||
case "turn.started":
|
||||
return PlayCircle;
|
||||
case "turn.ended":
|
||||
|
|
@ -40,6 +76,8 @@ export const getEventIcon = (type: string) => {
|
|||
return Activity;
|
||||
case "item.completed":
|
||||
return CheckCircle;
|
||||
|
||||
// Approval events
|
||||
case "question.requested":
|
||||
return HelpCircle;
|
||||
case "question.resolved":
|
||||
|
|
@ -48,11 +86,16 @@ export const getEventIcon = (type: string) => {
|
|||
return Shield;
|
||||
case "permission.resolved":
|
||||
return CheckCircle;
|
||||
|
||||
// Error events
|
||||
case "error":
|
||||
return AlertTriangle;
|
||||
case "agent.unparsed":
|
||||
return Brain;
|
||||
|
||||
default:
|
||||
if (type.startsWith("acp.")) return Zap;
|
||||
if (type.startsWith("inspector.")) return Info;
|
||||
if (type.startsWith("item.")) return MessageSquare;
|
||||
if (type.startsWith("session.")) return PlayCircle;
|
||||
if (type.startsWith("error")) return AlertTriangle;
|
||||
|
|
|
|||
790
frontend/packages/inspector/src/lib/legacyClient.ts
Normal file
790
frontend/packages/inspector/src/lib/legacyClient.ts
Normal file
|
|
@ -0,0 +1,790 @@
|
|||
import {
|
||||
SandboxAgent,
|
||||
type PermissionOption,
|
||||
type RequestPermissionRequest,
|
||||
type RequestPermissionResponse,
|
||||
type SandboxAgentAcpClient,
|
||||
type SandboxAgentConnectOptions,
|
||||
type SessionNotification,
|
||||
} from "sandbox-agent";
|
||||
import type {
|
||||
AgentInfo,
|
||||
AgentModelInfo,
|
||||
AgentModeInfo,
|
||||
AgentModelsResponse,
|
||||
AgentModesResponse,
|
||||
CreateSessionRequest,
|
||||
EventsQuery,
|
||||
EventsResponse,
|
||||
MessageRequest,
|
||||
PermissionEventData,
|
||||
PermissionReplyRequest,
|
||||
QuestionEventData,
|
||||
QuestionReplyRequest,
|
||||
SessionInfo,
|
||||
SessionListResponse,
|
||||
TurnStreamQuery,
|
||||
UniversalEvent,
|
||||
} from "../types/legacyApi";
|
||||
|
||||
type PendingPermission = {
|
||||
request: RequestPermissionRequest;
|
||||
resolve: (response: RequestPermissionResponse) => void;
|
||||
autoEndTurnOnResolve?: boolean;
|
||||
};
|
||||
|
||||
type PendingQuestion = {
|
||||
prompt: string;
|
||||
options: string[];
|
||||
autoEndTurnOnResolve?: boolean;
|
||||
};
|
||||
|
||||
type RuntimeSession = {
|
||||
aliasSessionId: string;
|
||||
realSessionId: string;
|
||||
agent: string;
|
||||
connection: SandboxAgentAcpClient;
|
||||
events: UniversalEvent[];
|
||||
nextSequence: number;
|
||||
listeners: Set<(event: UniversalEvent) => void>;
|
||||
info: SessionInfo;
|
||||
pendingPermissions: Map<string, PendingPermission>;
|
||||
pendingQuestions: Map<string, PendingQuestion>;
|
||||
};
|
||||
|
||||
const TDOO_PERMISSION_MODE =
|
||||
"TDOO: ACP permission mode preconfiguration is not implemented in inspector compatibility.";
|
||||
const TDOO_VARIANT =
|
||||
"TDOO: ACP session variants are not implemented in inspector compatibility.";
|
||||
const TDOO_SKILLS =
|
||||
"TDOO: ACP skills source configuration is not implemented in inspector compatibility.";
|
||||
const TDOO_MODE_DISCOVERY =
|
||||
"TDOO: ACP mode discovery before session creation is not implemented; returning cached/empty modes.";
|
||||
const TDOO_MODEL_DISCOVERY =
|
||||
"TDOO: ACP model discovery before session creation is not implemented; returning cached/empty models.";
|
||||
|
||||
export class InspectorLegacyClient {
|
||||
private readonly base: SandboxAgent;
|
||||
private readonly sessions = new Map<string, RuntimeSession>();
|
||||
private readonly aliasByRealSessionId = new Map<string, string>();
|
||||
private readonly modeCache = new Map<string, AgentModeInfo[]>();
|
||||
private readonly modelCache = new Map<string, AgentModelsResponse>();
|
||||
private permissionCounter = 0;
|
||||
|
||||
private constructor(base: SandboxAgent) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
static async connect(options: SandboxAgentConnectOptions): Promise<InspectorLegacyClient> {
|
||||
const base = await SandboxAgent.connect(options);
|
||||
return new InspectorLegacyClient(base);
|
||||
}
|
||||
|
||||
async getHealth() {
|
||||
return this.base.getHealth();
|
||||
}
|
||||
|
||||
async listAgents(): Promise<{ agents: AgentInfo[] }> {
|
||||
const response = await this.base.listAgents();
|
||||
|
||||
return {
|
||||
agents: response.agents.map((agent) => {
|
||||
const installed =
|
||||
agent.agent_process_installed &&
|
||||
(!agent.native_required || agent.native_installed);
|
||||
return {
|
||||
id: agent.id,
|
||||
installed,
|
||||
credentialsAvailable: true,
|
||||
version: agent.agent_process_version ?? agent.native_version ?? null,
|
||||
path: null,
|
||||
capabilities: {
|
||||
unstable_methods: agent.capabilities.unstable_methods,
|
||||
},
|
||||
native_required: agent.native_required,
|
||||
native_installed: agent.native_installed,
|
||||
native_version: agent.native_version,
|
||||
agent_process_installed: agent.agent_process_installed,
|
||||
agent_process_source: agent.agent_process_source,
|
||||
agent_process_version: agent.agent_process_version,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async installAgent(agent: string, request: { reinstall?: boolean } = {}) {
|
||||
return this.base.installAgent(agent, request);
|
||||
}
|
||||
|
||||
async getAgentModes(agentId: string): Promise<AgentModesResponse> {
|
||||
const modes = this.modeCache.get(agentId);
|
||||
if (modes) {
|
||||
return { modes };
|
||||
}
|
||||
|
||||
console.warn(TDOO_MODE_DISCOVERY);
|
||||
return { modes: [] };
|
||||
}
|
||||
|
||||
async getAgentModels(agentId: string): Promise<AgentModelsResponse> {
|
||||
const models = this.modelCache.get(agentId);
|
||||
if (models) {
|
||||
return models;
|
||||
}
|
||||
|
||||
console.warn(TDOO_MODEL_DISCOVERY);
|
||||
return { models: [], defaultModel: null };
|
||||
}
|
||||
|
||||
async createSession(aliasSessionId: string, request: CreateSessionRequest): Promise<void> {
|
||||
await this.terminateSession(aliasSessionId).catch(() => {
|
||||
// Ignore if it doesn't exist yet.
|
||||
});
|
||||
|
||||
const acp = await this.base.createAcpClient({
|
||||
agent: request.agent,
|
||||
client: {
|
||||
sessionUpdate: async (notification) => {
|
||||
this.handleSessionUpdate(notification);
|
||||
},
|
||||
requestPermission: async (permissionRequest) => {
|
||||
return this.handlePermissionRequest(permissionRequest);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await acp.initialize();
|
||||
|
||||
const created = await acp.newSession({
|
||||
cwd: "/",
|
||||
mcpServers: convertMcpConfig(request.mcp ?? {}),
|
||||
});
|
||||
|
||||
if (created.modes?.availableModes) {
|
||||
this.modeCache.set(
|
||||
request.agent,
|
||||
created.modes.availableModes.map((mode) => ({
|
||||
id: mode.id,
|
||||
name: mode.name,
|
||||
description: mode.description ?? undefined,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (created.models?.availableModels) {
|
||||
this.modelCache.set(request.agent, {
|
||||
models: created.models.availableModels.map((model) => ({
|
||||
id: model.modelId,
|
||||
name: model.name,
|
||||
description: model.description ?? undefined,
|
||||
})),
|
||||
defaultModel: created.models.currentModelId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
const runtime: RuntimeSession = {
|
||||
aliasSessionId,
|
||||
realSessionId: created.sessionId,
|
||||
agent: request.agent,
|
||||
connection: acp,
|
||||
events: [],
|
||||
nextSequence: 1,
|
||||
listeners: new Set(),
|
||||
info: {
|
||||
sessionId: aliasSessionId,
|
||||
agent: request.agent,
|
||||
eventCount: 0,
|
||||
ended: false,
|
||||
model: request.model ?? null,
|
||||
variant: request.variant ?? null,
|
||||
permissionMode: request.permissionMode ?? null,
|
||||
mcp: request.mcp,
|
||||
skills: request.skills,
|
||||
},
|
||||
pendingPermissions: new Map(),
|
||||
pendingQuestions: new Map(),
|
||||
};
|
||||
|
||||
this.sessions.set(aliasSessionId, runtime);
|
||||
this.aliasByRealSessionId.set(created.sessionId, aliasSessionId);
|
||||
|
||||
if (request.agentMode) {
|
||||
try {
|
||||
await acp.setSessionMode({ sessionId: created.sessionId, modeId: request.agentMode });
|
||||
} catch {
|
||||
this.emitError(aliasSessionId, `TDOO: Unable to apply mode \"${request.agentMode}\" via ACP.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.model) {
|
||||
try {
|
||||
await acp.unstableSetSessionModel({
|
||||
sessionId: created.sessionId,
|
||||
modelId: request.model,
|
||||
});
|
||||
} catch {
|
||||
this.emitError(aliasSessionId, `TDOO: Unable to apply model \"${request.model}\" via ACP.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.permissionMode) {
|
||||
this.emitError(aliasSessionId, TDOO_PERMISSION_MODE);
|
||||
}
|
||||
|
||||
if (request.variant) {
|
||||
this.emitError(aliasSessionId, TDOO_VARIANT);
|
||||
}
|
||||
|
||||
if (request.skills?.sources && request.skills.sources.length > 0) {
|
||||
this.emitError(aliasSessionId, TDOO_SKILLS);
|
||||
}
|
||||
|
||||
this.emitEvent(aliasSessionId, "session.started", {
|
||||
session_id: aliasSessionId,
|
||||
agent: request.agent,
|
||||
});
|
||||
}
|
||||
|
||||
async listSessions(): Promise<SessionListResponse> {
|
||||
const sessions = Array.from(this.sessions.values()).map((session) => {
|
||||
return {
|
||||
...session.info,
|
||||
eventCount: session.events.length,
|
||||
};
|
||||
});
|
||||
|
||||
return { sessions };
|
||||
}
|
||||
|
||||
async postMessage(sessionId: string, request: MessageRequest): Promise<void> {
|
||||
const runtime = this.requireActiveSession(sessionId);
|
||||
const message = request.message.trim();
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emitEvent(sessionId, "inspector.turn_started", {
|
||||
session_id: sessionId,
|
||||
});
|
||||
|
||||
this.emitEvent(sessionId, "inspector.user_message", {
|
||||
session_id: sessionId,
|
||||
text: message,
|
||||
});
|
||||
|
||||
try {
|
||||
await runtime.connection.prompt({
|
||||
sessionId: runtime.realSessionId,
|
||||
prompt: [{ type: "text", text: message }],
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : "prompt failed";
|
||||
this.emitError(sessionId, detail);
|
||||
throw error;
|
||||
} finally {
|
||||
this.emitEvent(sessionId, "inspector.turn_ended", {
|
||||
session_id: sessionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getEvents(sessionId: string, query: EventsQuery = {}): Promise<EventsResponse> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
const offset = query.offset ?? 0;
|
||||
const limit = query.limit ?? 200;
|
||||
|
||||
const events = runtime.events.filter((event) => event.sequence > offset).slice(0, limit);
|
||||
return { events };
|
||||
}
|
||||
|
||||
async *streamEvents(
|
||||
sessionId: string,
|
||||
query: EventsQuery = {},
|
||||
signal?: AbortSignal,
|
||||
): AsyncIterable<UniversalEvent> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
let cursor = query.offset ?? 0;
|
||||
|
||||
for (const event of runtime.events) {
|
||||
if (event.sequence <= cursor) {
|
||||
continue;
|
||||
}
|
||||
cursor = event.sequence;
|
||||
yield event;
|
||||
}
|
||||
|
||||
const queue: UniversalEvent[] = [];
|
||||
let wake: (() => void) | null = null;
|
||||
|
||||
const listener = (event: UniversalEvent) => {
|
||||
if (event.sequence <= cursor) {
|
||||
return;
|
||||
}
|
||||
queue.push(event);
|
||||
if (wake) {
|
||||
wake();
|
||||
wake = null;
|
||||
}
|
||||
};
|
||||
|
||||
runtime.listeners.add(listener);
|
||||
|
||||
try {
|
||||
while (!signal?.aborted) {
|
||||
if (queue.length === 0) {
|
||||
await waitForSignalOrEvent(signal, () => {
|
||||
wake = () => {};
|
||||
return new Promise<void>((resolve) => {
|
||||
wake = resolve;
|
||||
});
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const next = queue.shift();
|
||||
if (!next) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor = next.sequence;
|
||||
yield next;
|
||||
}
|
||||
} finally {
|
||||
runtime.listeners.delete(listener);
|
||||
}
|
||||
}
|
||||
|
||||
async *streamTurn(
|
||||
sessionId: string,
|
||||
request: MessageRequest,
|
||||
_query?: TurnStreamQuery,
|
||||
signal?: AbortSignal,
|
||||
): AsyncIterable<UniversalEvent> {
|
||||
if (signal?.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtime = this.requireActiveSession(sessionId);
|
||||
let cursor = runtime.nextSequence - 1;
|
||||
const queue: UniversalEvent[] = [];
|
||||
let wake: (() => void) | null = null;
|
||||
let promptDone = false;
|
||||
let promptError: unknown = null;
|
||||
|
||||
const notify = () => {
|
||||
if (wake) {
|
||||
wake();
|
||||
wake = null;
|
||||
}
|
||||
};
|
||||
|
||||
const listener = (event: UniversalEvent) => {
|
||||
if (event.sequence <= cursor) {
|
||||
return;
|
||||
}
|
||||
queue.push(event);
|
||||
notify();
|
||||
};
|
||||
|
||||
runtime.listeners.add(listener);
|
||||
|
||||
const promptPromise = this.postMessage(sessionId, request)
|
||||
.catch((error) => {
|
||||
promptError = error;
|
||||
})
|
||||
.finally(() => {
|
||||
promptDone = true;
|
||||
notify();
|
||||
});
|
||||
|
||||
try {
|
||||
while (!signal?.aborted) {
|
||||
if (queue.length === 0) {
|
||||
if (promptDone) {
|
||||
break;
|
||||
}
|
||||
|
||||
await waitForSignalOrEvent(signal, () => {
|
||||
wake = () => {};
|
||||
return new Promise<void>((resolve) => {
|
||||
wake = resolve;
|
||||
});
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const next = queue.shift();
|
||||
if (!next) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor = next.sequence;
|
||||
yield next;
|
||||
}
|
||||
} finally {
|
||||
runtime.listeners.delete(listener);
|
||||
}
|
||||
|
||||
await promptPromise;
|
||||
if (promptError) {
|
||||
throw promptError;
|
||||
}
|
||||
}
|
||||
|
||||
async replyQuestion(
|
||||
sessionId: string,
|
||||
questionId: string,
|
||||
request: QuestionReplyRequest,
|
||||
): Promise<void> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
const pending = runtime.pendingQuestions.get(questionId);
|
||||
if (!pending) {
|
||||
throw new Error("TDOO: Question request no longer pending.");
|
||||
}
|
||||
|
||||
runtime.pendingQuestions.delete(questionId);
|
||||
const response = request.answers?.[0]?.[0] ?? null;
|
||||
const resolved: QuestionEventData & { response?: string | null } = {
|
||||
question_id: questionId,
|
||||
status: "resolved",
|
||||
prompt: pending.prompt,
|
||||
options: pending.options,
|
||||
response,
|
||||
};
|
||||
this.emitEvent(sessionId, "question.resolved", resolved);
|
||||
if (pending.autoEndTurnOnResolve) {
|
||||
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
async rejectQuestion(sessionId: string, questionId: string): Promise<void> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
const pending = runtime.pendingQuestions.get(questionId);
|
||||
if (!pending) {
|
||||
throw new Error("TDOO: Question request no longer pending.");
|
||||
}
|
||||
|
||||
runtime.pendingQuestions.delete(questionId);
|
||||
const resolved: QuestionEventData & { response?: string | null } = {
|
||||
question_id: questionId,
|
||||
status: "resolved",
|
||||
prompt: pending.prompt,
|
||||
options: pending.options,
|
||||
response: null,
|
||||
};
|
||||
this.emitEvent(sessionId, "question.resolved", resolved);
|
||||
if (pending.autoEndTurnOnResolve) {
|
||||
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
async replyPermission(
|
||||
sessionId: string,
|
||||
permissionId: string,
|
||||
request: PermissionReplyRequest,
|
||||
): Promise<void> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
const pending = runtime.pendingPermissions.get(permissionId);
|
||||
if (!pending) {
|
||||
throw new Error("TDOO: Permission request no longer pending.");
|
||||
}
|
||||
|
||||
const optionId = selectPermissionOption(pending.request.options, request.reply);
|
||||
const response: RequestPermissionResponse = optionId
|
||||
? {
|
||||
outcome: {
|
||||
outcome: "selected",
|
||||
optionId,
|
||||
},
|
||||
}
|
||||
: {
|
||||
outcome: {
|
||||
outcome: "cancelled",
|
||||
},
|
||||
};
|
||||
|
||||
pending.resolve(response);
|
||||
runtime.pendingPermissions.delete(permissionId);
|
||||
|
||||
const action = pending.request.toolCall.title ?? pending.request.toolCall.kind ?? "permission";
|
||||
const resolved: PermissionEventData = {
|
||||
permission_id: permissionId,
|
||||
status: "resolved",
|
||||
action,
|
||||
metadata: {
|
||||
reply: request.reply,
|
||||
},
|
||||
};
|
||||
|
||||
this.emitEvent(sessionId, "permission.resolved", resolved);
|
||||
if (pending.autoEndTurnOnResolve) {
|
||||
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
async terminateSession(sessionId: string): Promise<void> {
|
||||
const runtime = this.sessions.get(sessionId);
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emitEvent(sessionId, "session.ended", {
|
||||
reason: "terminated_by_user",
|
||||
terminated_by: "inspector",
|
||||
});
|
||||
|
||||
runtime.info.ended = true;
|
||||
|
||||
for (const pending of runtime.pendingPermissions.values()) {
|
||||
pending.resolve({
|
||||
outcome: {
|
||||
outcome: "cancelled",
|
||||
},
|
||||
});
|
||||
}
|
||||
runtime.pendingPermissions.clear();
|
||||
runtime.pendingQuestions.clear();
|
||||
|
||||
try {
|
||||
await runtime.connection.close();
|
||||
} catch {
|
||||
// Best-effort close.
|
||||
}
|
||||
|
||||
this.aliasByRealSessionId.delete(runtime.realSessionId);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
for (const sessionId of Array.from(this.sessions.keys())) {
|
||||
await this.terminateSession(sessionId);
|
||||
}
|
||||
|
||||
await this.base.dispose();
|
||||
}
|
||||
|
||||
private handleSessionUpdate(notification: SessionNotification): void {
|
||||
const aliasSessionId = this.aliasByRealSessionId.get(notification.sessionId);
|
||||
if (!aliasSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtime = this.sessions.get(aliasSessionId);
|
||||
if (!runtime || runtime.info.ended) {
|
||||
return;
|
||||
}
|
||||
|
||||
const update = notification.update;
|
||||
|
||||
// Still handle session_info_update for sidebar metadata
|
||||
if (update.sessionUpdate === "session_info_update") {
|
||||
runtime.info.title = update.title ?? runtime.info.title;
|
||||
runtime.info.updatedAt = update.updatedAt ?? runtime.info.updatedAt;
|
||||
}
|
||||
|
||||
// Emit the raw notification as the event data, using the ACP discriminator as the type
|
||||
this.emitEvent(aliasSessionId, `acp.${update.sessionUpdate}`, notification);
|
||||
}
|
||||
|
||||
private async handlePermissionRequest(
|
||||
request: RequestPermissionRequest,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
const aliasSessionId = this.aliasByRealSessionId.get(request.sessionId);
|
||||
if (!aliasSessionId) {
|
||||
return {
|
||||
outcome: {
|
||||
outcome: "cancelled",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const runtime = this.sessions.get(aliasSessionId);
|
||||
if (!runtime || runtime.info.ended) {
|
||||
return {
|
||||
outcome: {
|
||||
outcome: "cancelled",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
this.permissionCounter += 1;
|
||||
const permissionId = `permission-${this.permissionCounter}`;
|
||||
|
||||
const action = request.toolCall.title ?? request.toolCall.kind ?? "permission";
|
||||
const pendingEvent: PermissionEventData = {
|
||||
permission_id: permissionId,
|
||||
status: "requested",
|
||||
action,
|
||||
metadata: request,
|
||||
};
|
||||
|
||||
this.emitEvent(aliasSessionId, "permission.requested", pendingEvent);
|
||||
|
||||
return await new Promise<RequestPermissionResponse>((resolve) => {
|
||||
runtime.pendingPermissions.set(permissionId, { request, resolve });
|
||||
});
|
||||
}
|
||||
|
||||
private emitError(sessionId: string, message: string): void {
|
||||
this.emitEvent(sessionId, "error", {
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
private emitEvent(sessionId: string, type: string, data: unknown): void {
|
||||
const runtime = this.sessions.get(sessionId);
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event: UniversalEvent = {
|
||||
event_id: `${sessionId}-${runtime.nextSequence}`,
|
||||
sequence: runtime.nextSequence,
|
||||
type,
|
||||
source: "inspector.acp",
|
||||
time: new Date().toISOString(),
|
||||
synthetic: true,
|
||||
data,
|
||||
};
|
||||
|
||||
runtime.nextSequence += 1;
|
||||
runtime.events.push(event);
|
||||
runtime.info.eventCount = runtime.events.length;
|
||||
|
||||
for (const listener of runtime.listeners) {
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
|
||||
private requireSession(sessionId: string): RuntimeSession {
|
||||
const runtime = this.sessions.get(sessionId);
|
||||
if (!runtime) {
|
||||
throw new Error(`Session not found: ${sessionId}`);
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private requireActiveSession(sessionId: string): RuntimeSession {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
if (runtime.info.ended) {
|
||||
throw new Error(`Session ended: ${sessionId}`);
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const convertMcpConfig = (mcp: Record<string, unknown>) => {
|
||||
return Object.entries(mcp)
|
||||
.map(([name, config]) => {
|
||||
if (!config || typeof config !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = config as Record<string, unknown>;
|
||||
const type = value.type;
|
||||
|
||||
if (type === "local") {
|
||||
const commandValue = value.command;
|
||||
const argsValue = value.args;
|
||||
|
||||
let command = "";
|
||||
let args: string[] = [];
|
||||
|
||||
if (Array.isArray(commandValue) && commandValue.length > 0) {
|
||||
command = String(commandValue[0] ?? "");
|
||||
args = commandValue.slice(1).map((part) => String(part));
|
||||
} else if (typeof commandValue === "string") {
|
||||
command = commandValue;
|
||||
}
|
||||
|
||||
if (Array.isArray(argsValue)) {
|
||||
args = argsValue.map((part) => String(part));
|
||||
}
|
||||
|
||||
const envObject =
|
||||
value.env && typeof value.env === "object" ? (value.env as Record<string, unknown>) : {};
|
||||
const env = Object.entries(envObject).map(([envName, envValue]) => ({
|
||||
name: envName,
|
||||
value: String(envValue),
|
||||
}));
|
||||
|
||||
return {
|
||||
name,
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "remote") {
|
||||
const headersObject =
|
||||
value.headers && typeof value.headers === "object"
|
||||
? (value.headers as Record<string, unknown>)
|
||||
: {};
|
||||
const headers = Object.entries(headersObject).map(([headerName, headerValue]) => ({
|
||||
name: headerName,
|
||||
value: String(headerValue),
|
||||
}));
|
||||
|
||||
return {
|
||||
type: "http" as const,
|
||||
name,
|
||||
url: String(value.url ?? ""),
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
||||
};
|
||||
|
||||
const selectPermissionOption = (
|
||||
options: PermissionOption[],
|
||||
reply: PermissionReplyRequest["reply"],
|
||||
): string | null => {
|
||||
const pick = (...kinds: PermissionOption["kind"][]) => {
|
||||
return options.find((option) => kinds.includes(option.kind))?.optionId ?? null;
|
||||
};
|
||||
|
||||
if (reply === "always") {
|
||||
return pick("allow_always", "allow_once");
|
||||
}
|
||||
|
||||
if (reply === "once") {
|
||||
return pick("allow_once", "allow_always");
|
||||
}
|
||||
|
||||
return pick("reject_once", "reject_always");
|
||||
};
|
||||
|
||||
const waitForSignalOrEvent = async (
|
||||
signal: AbortSignal | undefined,
|
||||
createWaitPromise: () => Promise<void>,
|
||||
) => {
|
||||
if (signal?.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
let done = false;
|
||||
const finish = () => {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
if (signal) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onAbort = () => finish();
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
|
||||
createWaitPromise().then(finish).catch(finish);
|
||||
});
|
||||
};
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import type { AgentCapabilities } from "sandbox-agent";
|
||||
|
||||
export type FeatureCoverageView = AgentCapabilities & {
|
||||
export type FeatureCoverageView = {
|
||||
unstable_methods?: boolean;
|
||||
planMode?: boolean;
|
||||
permissions?: boolean;
|
||||
questions?: boolean;
|
||||
toolCalls?: boolean;
|
||||
toolResults?: boolean;
|
||||
textMessages?: boolean;
|
||||
images?: boolean;
|
||||
|
|
@ -15,9 +18,11 @@ export type FeatureCoverageView = AgentCapabilities & {
|
|||
streamingDeltas?: boolean;
|
||||
itemStarted?: boolean;
|
||||
variants?: boolean;
|
||||
sharedProcess?: boolean;
|
||||
};
|
||||
|
||||
export const emptyFeatureCoverage: FeatureCoverageView = {
|
||||
unstable_methods: false,
|
||||
planMode: false,
|
||||
permissions: false,
|
||||
questions: false,
|
||||
|
|
|
|||
145
frontend/packages/inspector/src/types/legacyApi.ts
Normal file
145
frontend/packages/inspector/src/types/legacyApi.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
export type SkillSourceType = "github" | "local" | "git";
|
||||
|
||||
export type SkillSource = {
|
||||
type: SkillSourceType;
|
||||
source: string;
|
||||
skills?: string[];
|
||||
ref?: string;
|
||||
subpath?: string;
|
||||
};
|
||||
|
||||
export type CreateSessionRequest = {
|
||||
agent: string;
|
||||
agentMode?: string;
|
||||
permissionMode?: string;
|
||||
model?: string;
|
||||
variant?: string;
|
||||
mcp?: Record<string, unknown>;
|
||||
skills?: {
|
||||
sources: SkillSource[];
|
||||
};
|
||||
};
|
||||
|
||||
export type AgentModeInfo = {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type AgentModelInfo = {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
variants?: string[];
|
||||
};
|
||||
|
||||
export type AgentInfo = {
|
||||
id: string;
|
||||
installed: boolean;
|
||||
credentialsAvailable: boolean;
|
||||
version?: string | null;
|
||||
path?: string | null;
|
||||
capabilities: Record<string, boolean | undefined>;
|
||||
native_required?: boolean;
|
||||
native_installed?: boolean;
|
||||
native_version?: string | null;
|
||||
agent_process_installed?: boolean;
|
||||
agent_process_source?: string | null;
|
||||
agent_process_version?: string | null;
|
||||
};
|
||||
|
||||
export type ContentPart = {
|
||||
type?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type UniversalItem = {
|
||||
item_id: string;
|
||||
native_item_id?: string | null;
|
||||
parent_id?: string | null;
|
||||
kind: string;
|
||||
role?: string | null;
|
||||
content?: ContentPart[];
|
||||
status?: string | null;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type UniversalEvent = {
|
||||
event_id: string;
|
||||
sequence: number;
|
||||
type: string;
|
||||
source: string;
|
||||
time: string;
|
||||
synthetic?: boolean;
|
||||
data: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type PermissionEventData = {
|
||||
permission_id: string;
|
||||
status: "requested" | "resolved";
|
||||
action: string;
|
||||
metadata?: unknown;
|
||||
};
|
||||
|
||||
export type QuestionEventData = {
|
||||
question_id: string;
|
||||
status: "requested" | "resolved";
|
||||
prompt: string;
|
||||
options: string[];
|
||||
};
|
||||
|
||||
export type SessionInfo = {
|
||||
sessionId: string;
|
||||
agent: string;
|
||||
eventCount: number;
|
||||
ended?: boolean;
|
||||
model?: string | null;
|
||||
variant?: string | null;
|
||||
permissionMode?: string | null;
|
||||
mcp?: Record<string, unknown>;
|
||||
skills?: {
|
||||
sources?: SkillSource[];
|
||||
};
|
||||
title?: string | null;
|
||||
updatedAt?: string | null;
|
||||
};
|
||||
|
||||
export type EventsQuery = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
includeRaw?: boolean;
|
||||
};
|
||||
|
||||
export type EventsResponse = {
|
||||
events: UniversalEvent[];
|
||||
};
|
||||
|
||||
export type SessionListResponse = {
|
||||
sessions: SessionInfo[];
|
||||
};
|
||||
|
||||
export type AgentModesResponse = {
|
||||
modes: AgentModeInfo[];
|
||||
};
|
||||
|
||||
export type AgentModelsResponse = {
|
||||
models: AgentModelInfo[];
|
||||
defaultModel?: string | null;
|
||||
};
|
||||
|
||||
export type MessageRequest = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type TurnStreamQuery = {
|
||||
includeRaw?: boolean;
|
||||
};
|
||||
|
||||
export type PermissionReplyRequest = {
|
||||
reply: "once" | "always" | "reject";
|
||||
};
|
||||
|
||||
export type QuestionReplyRequest = {
|
||||
answers: string[][];
|
||||
};
|
||||
|
|
@ -2,8 +2,10 @@ export type RequestLog = {
|
|||
id: number;
|
||||
method: string;
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
status?: number;
|
||||
responseBody?: string;
|
||||
time: string;
|
||||
curl: string;
|
||||
error?: string;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ export default defineConfig(({ command }) => ({
|
|||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/v2": {
|
||||
target: "http://localhost:2468",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/v1": {
|
||||
target: "http://localhost:2468",
|
||||
changeOrigin: true,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const faqs = [
|
|||
{
|
||||
question: 'How is session data persisted?',
|
||||
answer:
|
||||
"This SDK does not handle persisting session data. Events stream in a universal JSON schema that you can persist anywhere. Consider using Postgres or <a href='https://rivet.gg' target='_blank' rel='noopener noreferrer' class='text-orange-400 hover:underline'>Rivet Actors</a> for data persistence.",
|
||||
"This SDK does not handle persisting session data. In v2, traffic is ACP JSON-RPC over <code>/v2/rpc</code>; persist envelopes in your own storage if you need replay or auditing.",
|
||||
},
|
||||
{
|
||||
question: 'Can I run this locally or does it require a sandbox provider?',
|
||||
|
|
|
|||
316
pnpm-lock.yaml
generated
316
pnpm-lock.yaml
generated
|
|
@ -381,34 +381,6 @@ importers:
|
|||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
|
||||
resources/agent-schemas:
|
||||
dependencies:
|
||||
'@anthropic-ai/claude-code':
|
||||
specifier: latest
|
||||
version: 2.1.37
|
||||
'@openai/codex':
|
||||
specifier: latest
|
||||
version: 0.98.0
|
||||
cheerio:
|
||||
specifier: ^1.0.0
|
||||
version: 1.2.0
|
||||
ts-json-schema-generator:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
devDependencies:
|
||||
'@types/json-schema':
|
||||
specifier: ^7.0.15
|
||||
version: 7.0.15
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.7
|
||||
tsx:
|
||||
specifier: ^4.19.0
|
||||
version: 4.21.0
|
||||
|
||||
resources/vercel-ai-sdk-schemas:
|
||||
dependencies:
|
||||
semver:
|
||||
|
|
@ -478,6 +450,25 @@ importers:
|
|||
specifier: latest
|
||||
version: 5.9.3
|
||||
|
||||
sdks/acp-http-client:
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk':
|
||||
specifier: ^0.14.1
|
||||
version: 0.14.1(zod@4.3.6)
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.7
|
||||
tsup:
|
||||
specifier: ^8.0.0
|
||||
version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
sdks/cli:
|
||||
dependencies:
|
||||
'@sandbox-agent/cli-shared':
|
||||
|
|
@ -567,6 +558,9 @@ importers:
|
|||
'@sandbox-agent/cli-shared':
|
||||
specifier: workspace:*
|
||||
version: link:../cli-shared
|
||||
acp-http-client:
|
||||
specifier: workspace:*
|
||||
version: link:../acp-http-client
|
||||
optionalDependencies:
|
||||
'@sandbox-agent/cli':
|
||||
specifier: workspace:*
|
||||
|
|
@ -590,15 +584,15 @@ importers:
|
|||
|
||||
packages:
|
||||
|
||||
'@agentclientprotocol/sdk@0.14.1':
|
||||
resolution: {integrity: sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w==}
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@anthropic-ai/claude-code@2.1.37':
|
||||
resolution: {integrity: sha512-YNrhAhWh/WAXAibZWfGBIUcMp+5caHGJKPkOjKSgYnCNQf7f+fP7eVTF1tr5FvvEksk2d9/HJgnh1fqOo1mP/A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@astrojs/compiler@2.13.0':
|
||||
resolution: {integrity: sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==}
|
||||
|
||||
|
|
@ -1800,65 +1794,33 @@ packages:
|
|||
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.33.5':
|
||||
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.0.4':
|
||||
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.0.4':
|
||||
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.0.4':
|
||||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
|
|
@ -1879,54 +1841,27 @@ packages:
|
|||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm@0.33.5':
|
||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
|
@ -1951,36 +1886,18 @@ packages:
|
|||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-x64@0.33.5':
|
||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
|
@ -2004,12 +1921,6 @@ packages:
|
|||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.33.5':
|
||||
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
|
@ -2076,11 +1987,6 @@ packages:
|
|||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@openai/codex@0.98.0':
|
||||
resolution: {integrity: sha512-CKjrhAmzTvWn7Vbsi27iZRKBAJw9a7ZTTkWQDbLgQZP1weGbDIBk1r6wiLEp1ZmDO7w0fHPLYgnVspiOrYgcxg==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
'@oslojs/encoding@1.1.0':
|
||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||
|
||||
|
|
@ -2870,13 +2776,6 @@ packages:
|
|||
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
cheerio-select@2.1.0:
|
||||
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
|
||||
|
||||
cheerio@1.2.0:
|
||||
resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
|
@ -3146,9 +3045,6 @@ packages:
|
|||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
encoding-sniffer@0.2.1:
|
||||
resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
|
|
@ -3160,10 +3056,6 @@ packages:
|
|||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@7.0.1:
|
||||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
error-stack-parser-es@1.0.5:
|
||||
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
|
||||
|
||||
|
|
@ -3491,9 +3383,6 @@ packages:
|
|||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
|
||||
|
||||
http-cache-semantics@4.2.0:
|
||||
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
|
||||
|
||||
|
|
@ -3505,10 +3394,6 @@ packages:
|
|||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
iconv-lite@0.7.2:
|
||||
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -4024,12 +3909,6 @@ packages:
|
|||
resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
||||
|
||||
parse5-parser-stream@7.1.2:
|
||||
resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==}
|
||||
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
|
|
@ -4969,15 +4848,6 @@ packages:
|
|||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||
engines: {node: '>=18'}
|
||||
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||
|
||||
whatwg-mimetype@4.0.0:
|
||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
which-pm-runs@1.1.0:
|
||||
resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -5128,18 +4998,11 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
'@agentclientprotocol/sdk@0.14.1(zod@4.3.6)':
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
'@anthropic-ai/claude-code@2.1.37':
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.33.5
|
||||
'@img/sharp-darwin-x64': 0.33.5
|
||||
'@img/sharp-linux-arm': 0.33.5
|
||||
'@img/sharp-linux-arm64': 0.33.5
|
||||
'@img/sharp-linux-x64': 0.33.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.33.5
|
||||
'@img/sharp-linuxmusl-x64': 0.33.5
|
||||
'@img/sharp-win32-x64': 0.33.5
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@astrojs/compiler@2.13.0': {}
|
||||
|
||||
|
|
@ -6383,47 +6246,25 @@ snapshots:
|
|||
|
||||
'@img/colour@1.0.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -6436,39 +6277,20 @@ snapshots:
|
|||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.0.5
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
|
|
@ -6489,31 +6311,16 @@ snapshots:
|
|||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
|
|
@ -6530,9 +6337,6 @@ snapshots:
|
|||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.33.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
|
|
@ -6615,8 +6419,6 @@ snapshots:
|
|||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.20.1
|
||||
|
||||
'@openai/codex@0.98.0': {}
|
||||
|
||||
'@oslojs/encoding@1.1.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
|
|
@ -7639,29 +7441,6 @@ snapshots:
|
|||
|
||||
check-error@2.1.3: {}
|
||||
|
||||
cheerio-select@2.1.0:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
css-select: 5.2.2
|
||||
css-what: 6.2.2
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
|
||||
cheerio@1.2.0:
|
||||
dependencies:
|
||||
cheerio-select: 2.1.0
|
||||
dom-serializer: 2.0.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
encoding-sniffer: 0.2.1
|
||||
htmlparser2: 10.1.0
|
||||
parse5: 7.3.0
|
||||
parse5-htmlparser2-tree-adapter: 7.1.0
|
||||
parse5-parser-stream: 7.1.2
|
||||
undici: 7.19.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
|
|
@ -7912,11 +7691,6 @@ snapshots:
|
|||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
encoding-sniffer@0.2.1:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
whatwg-encoding: 3.1.1
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
|
@ -7925,8 +7699,6 @@ snapshots:
|
|||
|
||||
entities@6.0.1: {}
|
||||
|
||||
entities@7.0.1: {}
|
||||
|
||||
error-stack-parser-es@1.0.5: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
|
@ -8449,13 +8221,6 @@ snapshots:
|
|||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
entities: 7.0.1
|
||||
|
||||
http-cache-semantics@4.2.0: {}
|
||||
|
||||
http-errors@2.0.1:
|
||||
|
|
@ -8468,10 +8233,6 @@ snapshots:
|
|||
|
||||
human-signals@5.0.0: {}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
iconv-lite@0.7.2:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
|
@ -9113,15 +8874,6 @@ snapshots:
|
|||
|
||||
parse-passwd@1.0.0: {}
|
||||
|
||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
parse5: 7.3.0
|
||||
|
||||
parse5-parser-stream@7.1.2:
|
||||
dependencies:
|
||||
parse5: 7.3.0
|
||||
|
||||
parse5@7.3.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
|
@ -10303,12 +10055,6 @@ snapshots:
|
|||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
whatwg-mimetype@4.0.0: {}
|
||||
|
||||
which-pm-runs@1.1.0: {}
|
||||
|
||||
which@2.0.2:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ packages:
|
|||
- "sdks/cli/platforms/*"
|
||||
- "sdks/gigacode"
|
||||
- "sdks/gigacode/platforms/*"
|
||||
- "resources/agent-schemas"
|
||||
- "resources/vercel-ai-sdk-schemas"
|
||||
- "scripts/release"
|
||||
- "scripts/sandbox-testing"
|
||||
|
|
|
|||
53
research/acp/00-delete-first.md
Normal file
53
research/acp/00-delete-first.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Delete Or Comment Out First
|
||||
|
||||
This is the initial, deliberate teardown list before building ACP-native v2.
|
||||
|
||||
## Hard delete first (in-house protocol types and converters)
|
||||
|
||||
- `server/packages/universal-agent-schema/Cargo.toml`
|
||||
- `server/packages/universal-agent-schema/src/lib.rs`
|
||||
- `server/packages/universal-agent-schema/src/agents/mod.rs`
|
||||
- `server/packages/universal-agent-schema/src/agents/claude.rs`
|
||||
- `server/packages/universal-agent-schema/src/agents/codex.rs`
|
||||
- `server/packages/universal-agent-schema/src/agents/opencode.rs`
|
||||
- `server/packages/universal-agent-schema/src/agents/amp.rs`
|
||||
- `spec/universal-schema.json`
|
||||
- `docs/session-transcript-schema.mdx`
|
||||
- `docs/conversion.mdx`
|
||||
|
||||
## Hard delete next (generated schema pipeline used only for in-house normalization)
|
||||
|
||||
- `server/packages/extracted-agent-schemas/Cargo.toml`
|
||||
- `server/packages/extracted-agent-schemas/build.rs`
|
||||
- `server/packages/extracted-agent-schemas/src/lib.rs`
|
||||
- `server/packages/extracted-agent-schemas/tests/schema_roundtrip.rs`
|
||||
- `resources/agent-schemas/` (entire folder)
|
||||
|
||||
## Remove/replace immediately (v1 hard removal)
|
||||
|
||||
- `server/packages/sandbox-agent/src/router.rs`: remove `/v1` handlers and replace with a unified `410 v1 removed` handler.
|
||||
- `server/packages/sandbox-agent/src/cli.rs`: remove/disable `api` subcommands that target `/v1`.
|
||||
- `sdks/typescript/src/client.ts`: methods bound to `/v1/*` routes.
|
||||
- `sdks/typescript/src/generated/openapi.ts`: current v1 OpenAPI output.
|
||||
- `docs/openapi.json`: current v1 OpenAPI document.
|
||||
|
||||
## Compatibility surface to disable during ACP core
|
||||
|
||||
- `server/packages/sandbox-agent/src/opencode_compat.rs`
|
||||
- `server/packages/sandbox-agent/tests/opencode-compat/`
|
||||
- `docs/opencode-compatibility.mdx`
|
||||
|
||||
Rationale: this layer is based on current v1 session/event model. Comment it out/disable it during ACP core implementation to avoid coupling and drift.
|
||||
|
||||
Important: OpenCode <-> ACP support is still required, but it is explicitly reintroduced in Phase 7 after ACP v2 core transport/runtime are stable.
|
||||
|
||||
## Tests to remove or disable with v1
|
||||
|
||||
- `server/packages/sandbox-agent/tests/http/`
|
||||
- `server/packages/sandbox-agent/tests/sessions/`
|
||||
- `server/packages/sandbox-agent/tests/agent-flows/`
|
||||
- `server/packages/sandbox-agent/tests/http_endpoints.rs`
|
||||
- `server/packages/sandbox-agent/tests/sessions.rs`
|
||||
- `server/packages/sandbox-agent/tests/agent_flows.rs`
|
||||
|
||||
Replace with ACP-native contract tests in v2.
|
||||
41
research/acp/README.md
Normal file
41
research/acp/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# ACP Migration Research
|
||||
|
||||
This folder captures the v2 migration plan from the current in-house protocol to ACP-first architecture.
|
||||
|
||||
## Files
|
||||
|
||||
- `research/acp/00-delete-first.md`: delete/comment-out-first inventory for the rewrite kickoff.
|
||||
- `research/acp/acp-notes.md`: ACP protocol notes extracted from `~/misc/acp-docs`.
|
||||
- `research/acp/acp-over-http-findings.md`: field research from ACP Zulip thread on real ACP-over-HTTP transport patterns and recommendations.
|
||||
- `research/acp/spec.md`: proposed v2 protocol/transport spec (ACP over HTTP).
|
||||
- `research/acp/v1-schema-to-acp-mapping.md`: exhaustive 1:1 mapping of all current v1 endpoints/events into ACP methods, notifications, responses, and `_meta` extensions.
|
||||
- `research/acp/rfds-vs-extensions.md`: simple list of which gaps should be raised as ACP RFDs vs remain product-specific extensions.
|
||||
- `research/acp/migration-steps.md`: concrete implementation phases and execution checklist.
|
||||
- `research/acp/friction.md`: ongoing friction/issues log for ACP migration decisions and blockers.
|
||||
|
||||
## Source docs read
|
||||
|
||||
- `~/misc/acp-docs/docs/protocol/overview.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/initialization.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/session-setup.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/prompt-turn.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/tool-calls.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/file-system.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/terminals.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/session-modes.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/session-config-options.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/extensibility.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/transports.mdx`
|
||||
- `~/misc/acp-docs/docs/protocol/schema.mdx`
|
||||
- `~/misc/acp-docs/schema/meta.json`
|
||||
- `~/misc/acp-docs/schema/schema.json`
|
||||
- `~/misc/acp-docs/docs/get-started/agents.mdx`
|
||||
- `~/misc/acp-docs/docs/get-started/registry.mdx`
|
||||
|
||||
## Important context
|
||||
|
||||
- ACP stable transport is stdio; streamable HTTP is still draft in ACP docs.
|
||||
- v2 in this repo is intentionally breaking and ACP-native.
|
||||
- v1 is removed in v2 and returns HTTP 410 on `/v1/*`.
|
||||
- `/opencode/*` is disabled during ACP core phases and re-enabled in the dedicated bridge phase.
|
||||
- Keep `research/acp/friction.md` current as issues/ambiguities are discovered.
|
||||
66
research/acp/acp-notes.md
Normal file
66
research/acp/acp-notes.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# ACP Notes (From Docs)
|
||||
|
||||
## Core protocol model
|
||||
|
||||
ACP is JSON-RPC 2.0 with bidirectional methods plus notifications.
|
||||
|
||||
Client to agent baseline methods:
|
||||
|
||||
- `initialize`
|
||||
- `authenticate` (optional if agent requires auth)
|
||||
- `session/new`
|
||||
- `session/prompt`
|
||||
- optional: `session/load`, `session/set_mode`, `session/set_config_option`
|
||||
- notification: `session/cancel`
|
||||
|
||||
Agent to client baseline method:
|
||||
|
||||
- `session/request_permission`
|
||||
|
||||
Agent to client optional methods:
|
||||
|
||||
- `fs/read_text_file`, `fs/write_text_file`
|
||||
- `terminal/create`, `terminal/output`, `terminal/wait_for_exit`, `terminal/kill`, `terminal/release`
|
||||
|
||||
Agent to client baseline notification:
|
||||
|
||||
- `session/update`
|
||||
|
||||
## Required protocol behavior
|
||||
|
||||
- Paths must be absolute.
|
||||
- Line numbers are 1-based.
|
||||
- Initialization must negotiate protocol version.
|
||||
- Capabilities omitted by peer must be treated as unsupported.
|
||||
|
||||
## Transport state
|
||||
|
||||
- ACP formally defines stdio transport today.
|
||||
- ACP docs mention streamable HTTP as draft/in progress.
|
||||
- Custom transports are allowed if JSON-RPC lifecycle semantics are preserved.
|
||||
|
||||
## Session lifecycle
|
||||
|
||||
- `session/new` creates session, returns `sessionId`.
|
||||
- `session/load` is optional and gated by `loadSession` capability.
|
||||
- `session/prompt` runs one turn and returns `stopReason`.
|
||||
- Streaming progress is entirely via `session/update` notifications.
|
||||
- Cancellation is `session/cancel` notification and must end with `stopReason=cancelled`.
|
||||
|
||||
## Tool and HITL model
|
||||
|
||||
- Tool calls are modeled through `session/update` (`tool_call`, `tool_call_update`).
|
||||
- HITL permission flow is a request/response RPC call (`session/request_permission`).
|
||||
|
||||
## ACP agent process relevance for this repo
|
||||
|
||||
From ACP docs agent list:
|
||||
|
||||
- Claude: ACP via agent process (`zed-industries/claude-code-acp`).
|
||||
- Codex: ACP via agent process (`zed-industries/codex-acp`).
|
||||
- OpenCode: ACP agent listed natively.
|
||||
|
||||
Gap to confirm for launch scope:
|
||||
|
||||
- Amp is not currently listed in ACP docs as a native ACP agent or published agent process.
|
||||
- We need an explicit product decision: block Amp in v2 launch or provide/build an ACP agent process.
|
||||
99
research/acp/acp-over-http-findings.md
Normal file
99
research/acp/acp-over-http-findings.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# ACP Over HTTP Findings (From Zulip Thread)
|
||||
|
||||
Date researched: 2026-02-10
|
||||
Thread: https://agentclientprotocol.zulipchat.com/#narrow/channel/543465-general/topic/ACP.20over.20HTTP/with/571476775
|
||||
|
||||
## Scope
|
||||
|
||||
This documents what people are actively piloting for ACP over HTTP and adjacent transports, based on the thread above (23 messages from 2026-01-09 to 2026-02-02).
|
||||
|
||||
## Key findings from the thread
|
||||
|
||||
1. There is no single settled transport yet; both Streamable HTTP and WebSocket are being actively piloted.
|
||||
2. A repeated concern is that ACP is more bidirectional than MCP, which makes HTTP request/response modeling trickier (especially permission requests and other server-initiated requests).
|
||||
3. MCP-style transport conventions are a strong reference point even when payloads remain ACP JSON-RPC.
|
||||
4. WebSocket pilots report low protocol adaptation cost from stdio-style ACP wiring.
|
||||
5. Streamable HTTP pilots are moving quickly and are considered important for ecosystem compatibility (existing HTTP infra, proxies, gateways, and remote-session resume UX).
|
||||
|
||||
## Concrete implementation patterns observed
|
||||
|
||||
## 1) Streamable HTTP profile (Goose)
|
||||
|
||||
Reference:
|
||||
- https://github.com/block/goose/pull/6741
|
||||
- https://github.com/block/goose/commit/274f6e3d7ed168ca8aa68c8683308086e01c88e6
|
||||
- https://github.com/block/goose/commit/54aff56c4662c14db79c34c057e991512fb6dcaf
|
||||
|
||||
Observed shape:
|
||||
- `POST /acp` for JSON-RPC input.
|
||||
- `GET /acp` optional long-lived SSE stream.
|
||||
- `DELETE /acp` to terminate session.
|
||||
- `Acp-Session-Id` header for connection/session binding.
|
||||
- `initialize` creates session + returns session header.
|
||||
- JSON-RPC request handled via SSE response stream.
|
||||
- JSON-RPC notifications/responses accepted with `202`.
|
||||
|
||||
Why it matters:
|
||||
- Very close to MCP Streamable HTTP request patterns while keeping ACP payloads.
|
||||
- Matches your goal to stay close to HTTP conventions already familiar to integrators.
|
||||
|
||||
## 2) WebSocket-first profile (JetBrains prototype, Agmente, others)
|
||||
|
||||
Thread references:
|
||||
- JetBrains prototype (Anna Zhdan): WebSocket worked naturally with few ACP protocol changes.
|
||||
- Agmente called out as using WebSockets for ACP.
|
||||
- Other teams reportedly piloting WebSockets for technical reasons.
|
||||
|
||||
Observed shape:
|
||||
- Single full-duplex socket carrying ACP JSON-RPC envelopes.
|
||||
- Simpler server-initiated requests and interleaving of notifications/responses.
|
||||
- Easier fanout/multiplexing (one report: `acp -> EventEmitter -> websocket`).
|
||||
|
||||
Why it matters:
|
||||
- Lower complexity for bidirectional ACP semantics.
|
||||
- But less aligned with strict HTTP-only environments without additional gatewaying.
|
||||
|
||||
## Recommended options for our v2
|
||||
|
||||
## Option A (recommended): Streamable HTTP as canonical v2 transport
|
||||
|
||||
Implement ACP over:
|
||||
- `POST /v2/rpc`
|
||||
- `GET /v2/rpc` (SSE, optional but recommended)
|
||||
- `DELETE /v2/rpc`
|
||||
|
||||
Profile:
|
||||
- Keep JSON-RPC payloads pure ACP.
|
||||
- Use `X-ACP-Connection-Id` (or `Acp-Session-Id`) for connection identity.
|
||||
- `initialize` without connection header creates agent process-backed connection.
|
||||
- JSON-RPC requests stream responses/events over SSE.
|
||||
- Notifications and JSON-RPC responses return `202 Accepted`.
|
||||
|
||||
Pros:
|
||||
- Best alignment with your stated direction ("same as ACP, over HTTP").
|
||||
- Integrates well with existing HTTP auth/proxy/gateway infrastructure.
|
||||
- Closer to MCP-style operational patterns teams already understand.
|
||||
|
||||
Cons:
|
||||
- More complex than WebSocket for bidirectional interleaving and timeout behavior.
|
||||
|
||||
## Option B: WebSocket as canonical transport + HTTP compatibility facade
|
||||
|
||||
Implement ACP internally over WebSocket semantics, then expose an HTTP facade for clients that require HTTP.
|
||||
|
||||
Pros:
|
||||
- Cleaner full-duplex behavior for ACP’s bidirectional model.
|
||||
- Potentially simpler core runtime behavior.
|
||||
|
||||
Cons:
|
||||
- Less direct fit to your immediate "ACP over HTTP v2 API" objective.
|
||||
- Requires and maintains a translation layer from day one.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Choose Option A for v2 launch and keep Option B as a later optimization path if operational pain appears.
|
||||
|
||||
Rationale:
|
||||
- It matches current product direction.
|
||||
- It aligns with concrete ecosystem work already visible (Goose Streamable HTTP).
|
||||
- It can still preserve a future WebSocket backend if needed later, without changing v2 public semantics.
|
||||
126
research/acp/extensibility-status.md
Normal file
126
research/acp/extensibility-status.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# ACP Session Extensibility Status
|
||||
|
||||
Status date: 2026-02-10
|
||||
|
||||
This document tracks v1 session-surface parity against ACP and defines the recommended extension strategy for features not covered by ACP stable methods.
|
||||
|
||||
Primary references:
|
||||
|
||||
- `research/acp/v1-schema-to-acp-mapping.md`
|
||||
- `~/misc/acp-docs/schema/meta.json` (stable methods)
|
||||
- `~/misc/acp-docs/schema/meta.unstable.json` (unstable methods)
|
||||
- `~/misc/acp-docs/docs/protocol/extensibility.mdx`
|
||||
|
||||
## 1) Status Matrix (Session-Centric)
|
||||
|
||||
| v1 capability (session-related) | ACP stable | ACP unstable | Status in v2 | Recommendation |
|
||||
|---|---|---|---|---|
|
||||
| Create session | `session/new` | N/A | Covered | Use ACP standard only. |
|
||||
| Load/replay prior session | `session/load` (capability-gated) | N/A | Covered when agent process supports `loadSession` | Keep standard behavior. |
|
||||
| Send message | `session/prompt` | N/A | Covered | Use ACP standard only. |
|
||||
| Stream updates | `session/update` | N/A | Covered | Use ACP standard only. |
|
||||
| Cancel in-flight turn | `session/cancel` | N/A | Covered | Use ACP standard only. |
|
||||
| Permission request/reply | `session/request_permission` | N/A | Covered | Use ACP standard only; map `once/always/reject` to option kinds. |
|
||||
| Session list | Not in stable | `session/list` | Agent-process-dependent | Prefer unstable when supported; fallback to `_sandboxagent/session/list`. |
|
||||
| Fork session | Not in stable | `session/fork` | Agent-process-dependent | Prefer unstable when supported; fallback extension only if needed. |
|
||||
| Resume session | Not in stable | `session/resume` | Agent-process-dependent | Prefer unstable when supported; fallback extension only if needed. |
|
||||
| Set session model | Not in stable | `session/set_model` | Agent-process-dependent | Prefer unstable when supported; otherwise use `session/set_config_option` when model config exists. |
|
||||
| Terminate session object | No direct method | N/A | Not covered | Add `_sandboxagent/session/terminate` only if product requires explicit termination semantics beyond turn cancel. |
|
||||
| Poll events/log (`/events`) | No direct method | N/A | Not covered | Avoid as primary flow; if required for compat tooling, add `_sandboxagent/session/events` as derived view over stream. |
|
||||
| Question request/reply/reject (generic HITL) | No generic question method | N/A | Not covered | Add `_sandboxagent/session/request_question` request/response extension. |
|
||||
| `skills` in create-session payload | No first-class field | N/A | Not covered | Carry in `_meta["sandboxagent.dev"].skills`; optionally add metadata patch extension for updates. |
|
||||
| `title` in create-session payload | No first-class field | N/A | Not covered | Carry in `_meta["sandboxagent.dev"].title`. |
|
||||
| `agentVersion` requested in create-session payload | No first-class field | N/A | Not covered | Carry in `_meta["sandboxagent.dev"].agentVersionRequested`. |
|
||||
| Client-chosen session ID alias | Agent returns canonical `sessionId` | N/A | Not covered | Carry in `_meta["sandboxagent.dev"].requestedSessionId`. |
|
||||
| `agentMode` | `session/set_mode` and `current_mode_update` | N/A | Covered when exposed by agent process | Prefer standard `session/set_mode`; fallback to config options. |
|
||||
| `model` field | `session/set_config_option` (category `model`) | `session/set_model` | Partially covered | Prefer config options, then unstable `session/set_model`, then `_meta` hint if agent process lacks both. |
|
||||
| `variant` field | `session/set_config_option` (category `thought_level` or custom) | N/A | Partially covered | Prefer config option category; fallback `_meta["sandboxagent.dev"].variant`. |
|
||||
| `permissionMode` field | No dedicated standard field | N/A | Partially covered | Represent as config option (category `mode` or custom category), else `_meta` hint. |
|
||||
| Attachments (`path`, `mime`, `filename`) | Prompt content blocks support resource/resource_link/mime | N/A | Mostly covered | Use content blocks; preserve `filename` in `_meta` when not represented natively. |
|
||||
|
||||
## 2) Recommended ACP Extension Strategy
|
||||
|
||||
Use ACP stable/unstable methods first, then extension methods (`_...`) and `_meta` per ACP extensibility rules.
|
||||
|
||||
Rules:
|
||||
|
||||
1. Prefer ACP standard methods and capability negotiation.
|
||||
2. Prefer ACP unstable method names where available and supported by agent process.
|
||||
3. Only add custom methods for product semantics ACP does not define.
|
||||
4. Keep custom data in `_meta["sandboxagent.dev"]`; do not add custom root fields.
|
||||
5. Advertise extension support in `initialize` capability `_meta`.
|
||||
|
||||
## 3) Recommended Extension Surface
|
||||
|
||||
### 3.1 Session metadata extension (for skills/title/version/aliases)
|
||||
|
||||
Use metadata first in `session/new.params._meta["sandboxagent.dev"]`:
|
||||
|
||||
```json
|
||||
{
|
||||
"requestedSessionId": "my-session-alias",
|
||||
"title": "Bugfix run",
|
||||
"skills": ["repo:vercel-labs/skills/nextjs"],
|
||||
"agentVersionRequested": "latest",
|
||||
"permissionMode": "ask",
|
||||
"variant": "high"
|
||||
}
|
||||
```
|
||||
|
||||
If runtime updates are needed after session creation, add:
|
||||
|
||||
- `_sandboxagent/session/set_metadata` (request)
|
||||
- `_sandboxagent/session/metadata_update` (notification)
|
||||
|
||||
### 3.2 Generic question HITL extension
|
||||
|
||||
Add:
|
||||
|
||||
- `_sandboxagent/session/request_question` (agent -> client request)
|
||||
- JSON-RPC response with `{ "outcome": "answered" | "rejected" | "cancelled", ... }`
|
||||
|
||||
Keep legacy bridge data in `_meta["sandboxagent.dev"]`:
|
||||
|
||||
- `questionId`
|
||||
- original option list
|
||||
- legacy status mapping
|
||||
|
||||
### 3.3 Session lifecycle extension (only where needed)
|
||||
|
||||
Add only if required by product UX:
|
||||
|
||||
- `_sandboxagent/session/terminate`
|
||||
- `_sandboxagent/session/events` (poll/read model over stream buffer)
|
||||
|
||||
Avoid treating these as primary data paths; ACP stream remains canonical.
|
||||
|
||||
### 3.4 Capability advertisement for extensions
|
||||
|
||||
Advertise extension support in `initialize.result.agentCapabilities._meta["sandboxagent.dev"]`:
|
||||
|
||||
```json
|
||||
{
|
||||
"extensions": {
|
||||
"sessionMetadata": true,
|
||||
"requestQuestion": true,
|
||||
"sessionTerminate": true,
|
||||
"sessionEventsPoll": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clients must feature-detect and degrade gracefully.
|
||||
|
||||
## 4) Recommendation for Current v2
|
||||
|
||||
Recommended implementation order:
|
||||
|
||||
1. Keep core session flow strictly ACP standard: `session/new`, `session/prompt`, `session/update`, `session/cancel`, `session/request_permission`.
|
||||
2. Use ACP unstable methods when agent processes advertise support (`session/list|fork|resume|set_model`).
|
||||
3. For `skills`, `title`, `requestedSessionId`, `agentVersion`, store and forward under `_meta["sandboxagent.dev"]`.
|
||||
4. Implement only two custom session extensions now:
|
||||
- `_sandboxagent/session/request_question`
|
||||
- `_sandboxagent/session/set_metadata` (if post-create metadata updates are required)
|
||||
5. Defer `_sandboxagent/session/terminate` and `_sandboxagent/session/events` unless a concrete consumer requires them.
|
||||
|
||||
This keeps the custom surface small while preserving v1-era product behavior.
|
||||
229
research/acp/friction.md
Normal file
229
research/acp/friction.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# ACP Migration Friction Log
|
||||
|
||||
Track every ACP migration issue that creates implementation friction, unclear behavior, or product risk.
|
||||
|
||||
Update this file continuously during the migration.
|
||||
|
||||
## Entry template
|
||||
|
||||
- Date:
|
||||
- Area:
|
||||
- Issue:
|
||||
- Impact:
|
||||
- Proposed direction:
|
||||
- Decision:
|
||||
- Owner:
|
||||
- Status: `open` | `in_progress` | `resolved` | `deferred`
|
||||
- Links:
|
||||
|
||||
## Entries
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Agent process availability
|
||||
- Issue: Amp does not have a confirmed official ACP agent process in current ACP docs/research.
|
||||
- Impact: Blocks full parity if Amp is required in v2 launch scope.
|
||||
- Proposed direction: Treat Amp as conditional for v2.0 and support via pinned fallback only if agent process source is validated.
|
||||
- Decision: Open.
|
||||
- Owner: Unassigned.
|
||||
- Status: open
|
||||
- Links: `research/acp/acp-notes.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Transport
|
||||
- Issue: ACP streamable HTTP is still draft upstream; v2 requires ACP over HTTP now.
|
||||
- Impact: Potential divergence from upstream HTTP semantics.
|
||||
- Proposed direction: Use strict JSON-RPC mapping and keep transport shim minimal/documented for later alignment.
|
||||
- Decision: Open.
|
||||
- Owner: Unassigned.
|
||||
- Status: open
|
||||
- Links: `research/acp/spec.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: OpenCode compatibility sequencing
|
||||
- Issue: OpenCode compatibility must be preserved but not block ACP core rewrite.
|
||||
- Impact: Risk of core rewrites being constrained by legacy compat behavior.
|
||||
- Proposed direction: Disable/comment out `/opencode/*` during ACP core bring-up, then re-enable via dedicated bridge step after core is stable.
|
||||
- Decision: Accepted.
|
||||
- Owner: Unassigned.
|
||||
- Status: in_progress
|
||||
- Links: `research/acp/migration-steps.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: TypeScript SDK layering
|
||||
- Issue: Risk of duplicating ACP protocol logic in our TS SDK instead of embedding upstream ACP SDK.
|
||||
- Impact: Drift from ACP semantics and higher maintenance cost.
|
||||
- Proposed direction: Embed `@agentclientprotocol/sdk` and keep our SDK as wrapper/convenience layer.
|
||||
- Decision: Accepted.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `research/acp/spec.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Installer behavior
|
||||
- Issue: Lazy agent process install can race under concurrent first-use requests.
|
||||
- Impact: Duplicate downloads, partial installs, or bootstrap failures.
|
||||
- Proposed direction: Add per-agent install lock + idempotent install path used by both explicit install and lazy install.
|
||||
- Decision: Accepted and implemented.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `research/acp/spec.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: ACP over HTTP standardization
|
||||
- Issue: Community is actively piloting both Streamable HTTP and WebSocket; no final single transport profile has emerged yet.
|
||||
- Impact: Risk of rework if we overfit to one draft behavior that later shifts.
|
||||
- Proposed direction: Lock v2 public contract to Streamable HTTP with ACP JSON-RPC payloads, keep implementation modular so WebSocket can be added later without breaking v2 API.
|
||||
- Decision: Accepted.
|
||||
- Owner: Unassigned.
|
||||
- Status: in_progress
|
||||
- Links: `research/acp/acp-over-http-findings.md`, `research/acp/spec.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Session lifecycle surface
|
||||
- Issue: ACP stable does not include v1-equivalent methods for session listing, explicit session termination/delete, or event-log polling.
|
||||
- Impact: Direct lift-and-shift of `/v1/sessions`, `/terminate`, and `/events` polling is not possible with ACP core only.
|
||||
- Proposed direction: Define `_sandboxagent/session/*` extension methods for these control operations, while keeping core prompt flow on standard ACP methods.
|
||||
- Decision: Open.
|
||||
- Owner: Unassigned.
|
||||
- Status: open
|
||||
- Links: `research/acp/v1-schema-to-acp-mapping.md`, `research/acp/spec.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: HITL question flow
|
||||
- Issue: ACP stable defines `session/request_permission` but not a generic question request/response method matching v1 `question.*` and question reply endpoints.
|
||||
- Impact: Existing question UX cannot be represented with standard ACP methods alone.
|
||||
- Proposed direction: Introduce `_sandboxagent/session/request_question` extension request/response and carry legacy shape via `_meta`.
|
||||
- Decision: Open.
|
||||
- Owner: Unassigned.
|
||||
- Status: open
|
||||
- Links: `research/acp/v1-schema-to-acp-mapping.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Filesystem parity
|
||||
- Issue: ACP stable filesystem methods are text-only (`fs/read_text_file`, `fs/write_text_file`), while v1 exposes raw bytes plus directory operations.
|
||||
- Impact: Binary file reads/writes, archive upload, and directory management cannot map directly to ACP core.
|
||||
- Proposed direction: Use ACP standard methods for UTF-8 text paths; add `_sandboxagent/fs/*` extensions for binary and directory operations.
|
||||
- Decision: Open.
|
||||
- Owner: Unassigned.
|
||||
- Status: open
|
||||
- Links: `research/acp/v1-schema-to-acp-mapping.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: v1 decommissioning
|
||||
- Issue: Ambiguity between "comment out v1" and "remove v1" causes rollout confusion.
|
||||
- Impact: Risk of partial compatibility behavior and extra maintenance burden.
|
||||
- Proposed direction: Hard-remove v1 behavior and return a stable HTTP 410 error for all `/v1/*` routes.
|
||||
- Decision: Accepted.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `research/acp/spec.md`, `research/acp/migration-steps.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: TypeScript ACP-over-HTTP client support
|
||||
- Issue: Official ACP client SDK does not currently provide the exact Streamable HTTP transport behavior required by this project.
|
||||
- Impact: SDK cannot target `/v2/rpc` without additional transport implementation.
|
||||
- Proposed direction: Embed upstream ACP SDK types/lifecycle and implement a project transport agent process for ACP-over-HTTP.
|
||||
- Decision: Accepted.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `research/acp/spec.md`, `research/acp/migration-steps.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Inspector migration
|
||||
- Issue: Inspector currently depends on v1 session/event surfaces.
|
||||
- Impact: Inspector breaks after v1 removal unless migrated to ACP transport.
|
||||
- Proposed direction: Keep `/ui/` route and migrate inspector runtime calls to ACP-over-HTTP; add dedicated inspector ACP tests.
|
||||
- Decision: Accepted.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `research/acp/spec.md`, `research/acp/migration-steps.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Inspector asset embedding
|
||||
- Issue: If `cargo build` runs before `frontend/packages/inspector/dist` exists, the build script can cache inspector-disabled embedding state.
|
||||
- Impact: Local runs can serve `/ui/` as disabled even after inspector is built, unless Cargo reruns the build script.
|
||||
- Proposed direction: Improve build-script invalidation to detect dist directory appearance/disappearance without manual rebuild nudges.
|
||||
- Decision: Implemented by watching the inspector package directory in `build.rs` so Cargo reruns when dist appears/disappears.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `server/packages/sandbox-agent/build.rs`, `research/acp/todo.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Deterministic ACP install tests
|
||||
- Issue: Installer and lazy-install tests were coupled to the live ACP registry, causing non-deterministic test behavior.
|
||||
- Impact: Flaky CI and inability to reliably validate install provenance and lazy install flows.
|
||||
- Proposed direction: Add `SANDBOX_AGENT_ACP_REGISTRY_URL` override and drive tests with a local one-shot registry fixture.
|
||||
- Decision: Accepted and implemented.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `server/packages/agent-management/src/agents.rs`, `server/packages/sandbox-agent/tests/v2_api.rs`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Inspector E2E tooling
|
||||
- Issue: `agent-browser` invocation under pnpm emits npm env warnings (`store-dir`, `recursive`) during scripted runs.
|
||||
- Impact: No functional break, but noisy CI logs and possible future npm strictness risk.
|
||||
- Proposed direction: Keep `npx -y agent-browser` script for now; revisit pinning/install strategy if warnings become hard failures.
|
||||
- Decision: Accepted.
|
||||
- Owner: Unassigned.
|
||||
- Status: open
|
||||
- Links: `frontend/packages/inspector/tests/agent-browser.e2e.sh`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Real agent process matrix rollout
|
||||
- Issue: Full agent process smoke coverage requires provider credentials and installed real agent processes in CI/runtime environments.
|
||||
- Impact: Phase-6 "full matrix green" and "install+prompt+stream per agent process" cannot be marked complete in local-only runs.
|
||||
- Proposed direction: Keep deterministic agent process matrix in default CI (stub ACP agent processes for claude/codex/opencode) and run real credentialed agent processes in environment-specific jobs.
|
||||
- Decision: Accepted.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `research/acp/todo.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Inspector v1-to-v2 compatibility
|
||||
- Issue: Restored inspector UI expects legacy `/v1` session/event contracts that no longer exist in ACP-native v2.
|
||||
- Impact: Full parity would block migration; inspector would otherwise fail to run against v2.
|
||||
- Proposed direction: Keep the restored UI and bridge to ACP with a thin compatibility client (`src/lib/legacyClient.ts`), stubbing non-parity features with explicit `TDOO` markers.
|
||||
- Decision: Accepted.
|
||||
- Owner: Unassigned.
|
||||
- Status: open
|
||||
- Links: `frontend/packages/inspector/src/lib/legacyClient.ts`, `research/acp/inspector-unimplemented.md`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Multi-client session visibility + process sharing
|
||||
- Issue: Existing ACP runtime mapped one HTTP ACP connection to one dedicated agent process, which prevented global session visibility and increased process count.
|
||||
- Impact: Clients could not discover sessions created by other clients; process utilization scaled with connection count instead of agent type.
|
||||
- Proposed direction: Use one shared backend process per `AgentId`, maintain server-owned in-memory meta session registry across all connections, intercept `session/list` as a global aggregated view, and add an experimental detach extension (`_sandboxagent/session/detach`) for connection-level session detachment.
|
||||
- Decision: Accepted and implemented.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `server/packages/sandbox-agent/src/acp_runtime/mod.rs`, `server/packages/sandbox-agent/src/acp_runtime/mock.rs`, `server/packages/sandbox-agent/tests/v2_api.rs`, `server/packages/sandbox-agent/tests/v2_agent_process_matrix.rs`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: TypeScript SDK package split and ACP lifecycle
|
||||
- Issue: `sandbox-agent` SDK exposed ACP transport primitives directly (`createAcpClient`, raw envelope APIs, ACP type re-exports), making the public API ACP-heavy.
|
||||
- Impact: Harder to keep a simple Sandbox-facing API while still supporting protocol-faithful ACP HTTP behavior and Sandbox metadata/extensions.
|
||||
- Proposed direction: Split into `acp-http-client` (pure ACP HTTP transport/client) and `sandbox-agent` (`SandboxAgentClient`) as a thin wrapper with metadata/event conversion and extension helpers.
|
||||
- Decision: Accepted and implemented.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `research/acp/ts-client.md`, `sdks/acp-http-client/src/index.ts`, `sdks/typescript/src/client.ts`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Streamable HTTP transport contract
|
||||
- Issue: Ambiguity over whether `/v2/rpc` should track MCP transport negotiation (`POST` accepting SSE responses, multi-stream fanout) versus Sandbox Agent's simpler JSON-only POST contract.
|
||||
- Impact: Without an explicit contract, clients can assume incompatible Accept/media semantics and open duplicate GET streams that receive duplicate events.
|
||||
- Proposed direction: Define Sandbox Agent transport profile explicitly: `POST /v2/rpc` is JSON-only (`Content-Type` and `Accept` for `application/json`), `GET /v2/rpc` is SSE-only (`Accept: text/event-stream`), and allow only one active SSE stream per ACP connection id.
|
||||
- Decision: Accepted and implemented.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `server/packages/sandbox-agent/src/router.rs`, `server/packages/sandbox-agent/src/acp_runtime/mod.rs`, `server/packages/sandbox-agent/tests/v2_api/acp_transport.rs`, `docs/advanced/acp-http-client.mdx`
|
||||
|
||||
- Date: 2026-02-10
|
||||
- Area: Agent selection contract for ACP bootstrap/session creation
|
||||
- Issue: `x-acp-agent` bound agent selection to transport bootstrap, which conflicted with Sandbox Agent meta-session goals where one client can manage sessions across multiple agents.
|
||||
- Impact: Connections appeared agent-affine; agent selection was hidden in HTTP headers rather than explicit in ACP payload metadata.
|
||||
- Proposed direction: Hard-remove `x-acp-agent`; require `params._meta["sandboxagent.dev"].agent` on `initialize` and `session/new`, and require `params.agent` for agent-routed calls that have no resolvable `sessionId`.
|
||||
- Decision: Accepted and implemented.
|
||||
- Owner: Unassigned.
|
||||
- Status: resolved
|
||||
- Links: `server/packages/sandbox-agent/src/router.rs`, `server/packages/sandbox-agent/src/acp_runtime/helpers.rs`, `server/packages/sandbox-agent/src/acp_runtime/mod.rs`, `server/packages/sandbox-agent/src/acp_runtime/ext_meta.rs`, `server/packages/sandbox-agent/tests/v2_api/acp_transport.rs`
|
||||
14
research/acp/inspector-unimplemented.md
Normal file
14
research/acp/inspector-unimplemented.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Inspector ACP Unimplemented
|
||||
|
||||
Updated: 2026-02-10
|
||||
|
||||
This tracks legacy inspector behaviors that do not yet have full parity on ACP v2.
|
||||
|
||||
1. TDOO: Session `permissionMode` preconfiguration on create is not wired in ACP inspector compatibility.
|
||||
2. TDOO: Session `variant` preconfiguration on create is not wired in ACP inspector compatibility.
|
||||
3. TDOO: Session `skills` source configuration is not wired in ACP inspector compatibility.
|
||||
4. TDOO: Question request/reply/reject flow is not implemented in ACP inspector compatibility.
|
||||
5. TDOO: Agent mode discovery before creating a session is not implemented (inspector currently returns cached-or-empty mode lists).
|
||||
6. TDOO: Agent model discovery before creating a session is not implemented (inspector currently returns cached-or-empty model lists).
|
||||
7. TDOO: Session listing only reflects sessions created by this inspector client instance (not full server/global session inventory).
|
||||
8. TDOO: Event history shown in inspector is synthesized from ACP traffic handled by the inspector compatibility layer, not the old canonical `/v1/sessions/*/events` backend history.
|
||||
242
research/acp/merge-acp.md
Normal file
242
research/acp/merge-acp.md
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
# Proposal: Move Static v2 HTTP Endpoints into ACP Extensions
|
||||
|
||||
## Goal
|
||||
|
||||
Keep `GET /v2/health` as the only static control endpoint, except for dedicated binary filesystem transfer endpoints.
|
||||
|
||||
Move all other current static v2 HTTP routes to ACP JSON-RPC methods (Sandbox Agent extensions under `_sandboxagent/...`) on `/v2/rpc`.
|
||||
|
||||
Retain these HTTP endpoints intentionally:
|
||||
|
||||
- `GET /v2/fs/file`
|
||||
- `PUT /v2/fs/file`
|
||||
- `POST /v2/fs/upload-batch`
|
||||
|
||||
No implementation in this proposal. This is a migration plan.
|
||||
|
||||
## Current State (from `server/packages/sandbox-agent/src/router.rs`)
|
||||
|
||||
Static v2 endpoints today:
|
||||
|
||||
- `GET /v2/agents`
|
||||
- `POST /v2/agents/:agent/install`
|
||||
- `GET /v2/sessions`
|
||||
- `GET /v2/sessions/:session_id`
|
||||
- `GET /v2/fs/entries`
|
||||
- `GET /v2/fs/file`
|
||||
- `PUT /v2/fs/file`
|
||||
- `DELETE /v2/fs/entry`
|
||||
- `POST /v2/fs/mkdir`
|
||||
- `POST /v2/fs/move`
|
||||
- `GET /v2/fs/stat`
|
||||
- `POST /v2/fs/upload-batch`
|
||||
|
||||
Non-static ACP transport endpoints (remain):
|
||||
|
||||
- `POST /v2/rpc`
|
||||
- `GET /v2/rpc` (SSE)
|
||||
- `DELETE /v2/rpc`
|
||||
|
||||
Health endpoint (remain):
|
||||
|
||||
- `GET /v2/health`
|
||||
|
||||
## Proposed Target Surface
|
||||
|
||||
Keep:
|
||||
|
||||
- `GET /v2/health`
|
||||
- `POST/GET/DELETE /v2/rpc`
|
||||
- `GET /v2/fs/file`
|
||||
- `PUT /v2/fs/file`
|
||||
- `POST /v2/fs/upload-batch`
|
||||
|
||||
Remove all other static v2 control/file routes after migration.
|
||||
|
||||
Add ACP extension methods:
|
||||
|
||||
- `_sandboxagent/agent/list`
|
||||
- `_sandboxagent/agent/install`
|
||||
- `_sandboxagent/session/list`
|
||||
- `_sandboxagent/session/get`
|
||||
- `_sandboxagent/fs/list_entries`
|
||||
- `_sandboxagent/fs/read_file` (parallel with HTTP)
|
||||
- `_sandboxagent/fs/write_file` (parallel with HTTP)
|
||||
- `_sandboxagent/fs/delete_entry`
|
||||
- `_sandboxagent/fs/mkdir`
|
||||
- `_sandboxagent/fs/move`
|
||||
- `_sandboxagent/fs/stat`
|
||||
- `_sandboxagent/fs/upload_batch` (parallel with HTTP)
|
||||
|
||||
Interpretation for clients: all agent/session operations and non-binary filesystem operations move to ACP extension calls over `/v2/rpc`. Binary file transfer has a dual surface: ACP equivalents exist in parallel, but HTTP remains the primary transport for large/streaming payloads.
|
||||
|
||||
## Endpoint-to-Method Mapping
|
||||
|
||||
| Existing HTTP | New ACP method | Notes |
|
||||
| --- | --- | --- |
|
||||
| `GET /v2/agents` | `_sandboxagent/agent/list` | Response keeps current `AgentListResponse` shape for low migration risk. |
|
||||
| `POST /v2/agents/:agent/install` | `_sandboxagent/agent/install` | Params include `agent`, `reinstall`, `agentVersion`, `agentProcessVersion`. |
|
||||
| `GET /v2/sessions` | `_sandboxagent/session/list` | Return current `SessionListResponse` shape (not ACP unstable list shape). |
|
||||
| `GET /v2/sessions/:session_id` | `_sandboxagent/session/get` | Return current `SessionInfo` shape; error on missing session. |
|
||||
| `GET /v2/fs/entries` | `_sandboxagent/fs/list_entries` | Preserve path + optional `sessionId` resolution semantics. |
|
||||
| `GET /v2/fs/file` | keep HTTP + `_sandboxagent/fs/read_file` | HTTP is primary because responses may require large streaming reads; ACP variant exists for compatibility/smaller payloads. |
|
||||
| `PUT /v2/fs/file` | keep HTTP + `_sandboxagent/fs/write_file` | HTTP is primary for large binary writes; ACP variant exists for compatibility/smaller payloads. |
|
||||
| `DELETE /v2/fs/entry` | `_sandboxagent/fs/delete_entry` | Preserve recursive directory delete behavior. |
|
||||
| `POST /v2/fs/mkdir` | `_sandboxagent/fs/mkdir` | Preserve create-dir behavior. |
|
||||
| `POST /v2/fs/move` | `_sandboxagent/fs/move` | Preserve `overwrite` behavior. |
|
||||
| `GET /v2/fs/stat` | `_sandboxagent/fs/stat` | Preserve `FsStat` shape. |
|
||||
| `POST /v2/fs/upload-batch` | keep HTTP + `_sandboxagent/fs/upload_batch` | HTTP is primary for large tar uploads; ACP variant exists for compatibility/smaller payloads. |
|
||||
|
||||
## ACP Contract Details
|
||||
|
||||
### Capability Advertisement
|
||||
|
||||
Extend initialize metadata (`_meta[sandboxagent.dev].extensions`) in `acp_runtime/ext_meta.rs` with booleans + method names for all new methods above, same pattern as existing:
|
||||
|
||||
- `sessionDetach`, `sessionTerminate`, `sessionListModels`, `sessionSetMetadata`, etc.
|
||||
|
||||
Add keys for new extensions (`agentList`, `agentInstall`, `fsListEntries`, `fsStat`, ...).
|
||||
|
||||
### Filesystem Exception (Intentional)
|
||||
|
||||
`GET/PUT /v2/fs/file` and `POST /v2/fs/upload-batch` stay as first-class Sandbox Agent HTTP APIs.
|
||||
|
||||
Reason:
|
||||
|
||||
- These operations are host/runtime capabilities implemented by Sandbox Agent, not agent-process behavior.
|
||||
- Keeping them server-owned gives consistent behavior across agents.
|
||||
- ACP envelopes are JSON-RPC payloads and are not suitable for streaming very large binary files efficiently.
|
||||
- `GET /v2/fs/file` specifically needs efficient streamed responses for large reads.
|
||||
|
||||
ACP parity note:
|
||||
|
||||
- Maintain ACP extension equivalents in parallel (`_sandboxagent/fs/read_file`, `_sandboxagent/fs/write_file`, `_sandboxagent/fs/upload_batch`) for compatibility.
|
||||
- ACP and HTTP variants should call the same underlying filesystem service code path to keep behavior consistent.
|
||||
- ACP variants are not intended for very large file transfer workloads.
|
||||
|
||||
### Error Mapping
|
||||
|
||||
Keep existing `SandboxError -> ProblemDetails` semantics over HTTP transport. For extension methods, surface structured JSON-RPC error payloads that map to existing invalid request / not found behavior.
|
||||
|
||||
## TypeScript Client Impact Assessment
|
||||
|
||||
Current behavior in `sdks/typescript/src/client.ts`:
|
||||
|
||||
- `listAgents` and `installAgent` call static HTTP endpoints.
|
||||
- `listSessions` and `getSession` call static HTTP endpoints.
|
||||
- FS helpers (`listFsEntries`, `readFsFile`, `writeFsFile`, `deleteFsEntry`, `mkdirFs`, `moveFs`, `statFs`, `uploadFsBatch`) call static HTTP endpoints.
|
||||
- ACP/session methods already use ACP (`newSession`, `loadSession`, `prompt`, etc).
|
||||
|
||||
Required change for ACP-only behavior:
|
||||
|
||||
- Reimplement non-binary helpers above as ACP extension wrappers via `acp.extMethod(...)`.
|
||||
- Keep method names stable in `SandboxAgentClient` to minimize user breakage.
|
||||
- Make ACP-backed helpers connection-scoped (same as ACP methods): they must throw `NotConnectedError` when disconnected.
|
||||
- Keep direct HTTP helper calls only for:
|
||||
- `getHealth()`
|
||||
- `readFsFile()` (`GET /v2/fs/file`)
|
||||
- `writeFsFile()` (`PUT /v2/fs/file`)
|
||||
- `uploadFsBatch()` (`POST /v2/fs/upload-batch`)
|
||||
- Keep ACP variants available through low-level `extMethod(...)` for advanced/smaller-payload use cases, but do not make them the SDK default path.
|
||||
|
||||
Package boundary after migration:
|
||||
|
||||
- `acp-http-client` remains protocol-pure ACP transport and generic `extMethod`/`extNotification`.
|
||||
- `sandbox-agent` remains the typed wrapper that maps convenience methods to `_sandboxagent/...` extension methods.
|
||||
- No direct `/v2/agents*`, `/v2/sessions*`, or non-binary `/v2/fs/*` fetches in SDK runtime code.
|
||||
- Binary file transfer keeps direct HTTP fetches on the three endpoints listed above.
|
||||
- SDK policy: prefer HTTP for `readFsFile`/`writeFsFile`/`uploadFsBatch` even if ACP extension variants exist.
|
||||
|
||||
Type changes expected in `sdks/typescript/src/types.ts`:
|
||||
|
||||
- Add typed request/response interfaces for new ACP extension methods.
|
||||
- Keep compatibility aliases where needed (`bytes_written` and `bytesWritten`, etc.) for one migration window.
|
||||
|
||||
Integration test impact (`sdks/typescript/tests/integration.test.ts`):
|
||||
|
||||
- Replace assumptions that agent/session/fs helpers are usable without ACP connection.
|
||||
- Add coverage that helpers work after `connect()` and use ACP extension paths end-to-end.
|
||||
- Keep real-server runtime tests (no fetch mocks for server behavior).
|
||||
|
||||
## Bootstrap Model (Important)
|
||||
|
||||
Today, first call without `x-acp-connection-id` must be `initialize`, and requires `params._meta["sandboxagent.dev"].agent`.
|
||||
|
||||
Implication after migration:
|
||||
|
||||
- Agent and ACP-backed filesystem control methods must run on an ACP connection.
|
||||
- Bootstrap flow should use `initialize` with `_meta["sandboxagent.dev"].agent = "mock"` for control-plane-only clients before calling extension methods.
|
||||
|
||||
Alternative (optional): introduce a runtime-only control connection mode that does not require backend agent init. This is a larger behavior change and can be deferred.
|
||||
|
||||
## Phased Migration Plan
|
||||
|
||||
### Phase 1: Add ACP Extension Equivalents
|
||||
|
||||
- Add methods to runtime extension handlers (`acp_runtime/ext_methods.rs`).
|
||||
- Reuse existing router/support mapping logic where possible to keep response parity.
|
||||
- Keep binary file-transfer ACP methods in parallel with HTTP (`_sandboxagent/fs/read_file`, `_sandboxagent/fs/write_file`, `_sandboxagent/fs/upload_batch`) and route both surfaces through shared implementation code.
|
||||
- Advertise new capabilities in `acp_runtime/ext_meta.rs`.
|
||||
- Add ACP extension tests for each new method in `server/packages/sandbox-agent/tests/v2_api/acp_extensions.rs`.
|
||||
|
||||
### Phase 2: Migrate Clients (No HTTP Route Removal Yet)
|
||||
|
||||
- TypeScript SDK (`sdks/typescript/src/client.ts`):
|
||||
- Repoint `listAgents`, `installAgent`, `listSessions`, `getSession`, `listFsEntries`, `deleteFsEntry`, `mkdirFs`, `moveFs`, and `statFs` to ACP extension calls.
|
||||
- Keep `readFsFile`, `writeFsFile`, and `uploadFsBatch` on HTTP endpoints.
|
||||
- Remove direct runtime fetch usage for `/v2/agents*`, `/v2/sessions*`, and non-binary `/v2/fs/*`.
|
||||
- Keep method names stable for callers.
|
||||
- Move these methods to connected-only semantics (`NotConnectedError` when disconnected).
|
||||
- CLI (`server/packages/sandbox-agent/src/cli.rs`):
|
||||
- Make `api agents list/install` call ACP extension methods (via ACP post flow), not direct `/v2/agents*` HTTP calls.
|
||||
- Inspector flow/docs:
|
||||
- Stop depending on `GET /v2/agents` in startup path; use ACP extension instead.
|
||||
|
||||
### Phase 3: Remove Static Endpoints (Except Health + Binary FS Transfer)
|
||||
|
||||
- Remove route registrations for `/v2/agents*`, `/v2/sessions*`, `/v2/fs/entries`, `/v2/fs/entry`, `/v2/fs/mkdir`, `/v2/fs/move`, `/v2/fs/stat` from `router.rs`.
|
||||
- Keep `/v2/health`, `/v2/rpc`, `GET /v2/fs/file`, `PUT /v2/fs/file`, and `POST /v2/fs/upload-batch`.
|
||||
- Optional short deprecation period: convert removed routes to `410 Gone` with explicit extension method in `detail`.
|
||||
|
||||
### Phase 4: Docs/OpenAPI/Test Cleanup
|
||||
|
||||
- Regenerate `docs/openapi.json` (should now primarily describe `/v2/health`, `/v2/rpc`, and retained binary fs transfer endpoints).
|
||||
- Update:
|
||||
- `docs/cli.mdx`
|
||||
- `docs/inspector.mdx`
|
||||
- `docs/sdks/typescript.mdx`
|
||||
- Replace current `control_plane.rs` HTTP-route assertions with ACP-extension assertions.
|
||||
|
||||
## Validation Plan
|
||||
|
||||
Server:
|
||||
|
||||
- ACP extension integration tests for all new methods.
|
||||
- Auth parity checks (token required behavior unchanged).
|
||||
- Existing ACP transport tests unchanged and green.
|
||||
|
||||
SDK:
|
||||
|
||||
- Real-server integration tests verify moved helpers now use ACP extensions.
|
||||
- No fetch transport mocks for server behavior.
|
||||
|
||||
CLI:
|
||||
|
||||
- `api agents list/install` e2e tests validate ACP-backed behavior.
|
||||
|
||||
Inspector:
|
||||
|
||||
- Browser e2e (`agent-browser`) still passes with ACP-only startup path.
|
||||
|
||||
## Rollout Strategy
|
||||
|
||||
1. Ship Phase 1 + 2 behind no route removals.
|
||||
2. Verify SDK/CLI/Inspector consume ACP extensions in CI.
|
||||
3. Remove static endpoints in one cut (Phase 3).
|
||||
4. Land docs/openapi updates immediately with removal.
|
||||
|
||||
## Open Decisions
|
||||
|
||||
1. Should removed `/v2/agents*`, `/v2/sessions*`, and non-binary `/v2/fs/*` return `410` for one release or be dropped immediately?
|
||||
2. Do we keep a strict response-shape parity layer for session/file methods, or normalize to ACP-native shapes?
|
||||
3. Should `/` service-root remain as informational HTTP, or be treated as out-of-scope for this “only health static + binary fs transfer” policy?
|
||||
174
research/acp/migration-steps.md
Normal file
174
research/acp/migration-steps.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Concrete Migration Steps
|
||||
|
||||
## Phase Progression Rule
|
||||
|
||||
- Do not start the next phase until the current phase validation gate is green in local runs and CI.
|
||||
- If a gate fails, log the issue in `research/acp/friction.md` before proceeding.
|
||||
- “Green” means required tests pass end to end, not just unit tests.
|
||||
|
||||
## Consolidated Test Suites (Authoritative)
|
||||
|
||||
The migration test plan is intentionally collapsed to avoid duplicate coverage.
|
||||
|
||||
1. ACP protocol conformance
|
||||
2. Transport contract (`/v2/rpc`)
|
||||
3. End-to-end agent process matrix (core flow + cancel + HITL + streaming)
|
||||
4. Installer suite (explicit + lazy + registry/fallback provenance)
|
||||
5. Security/auth isolation
|
||||
6. TypeScript SDK end-to-end (embedded + server, embedding `@agentclientprotocol/sdk`)
|
||||
7. v1 removal contract suite (`/v1/*` returns HTTP 410 + stable payload)
|
||||
8. Inspector ACP suite (mandatory `agent-browser` end-to-end automation)
|
||||
9. OpenCode <-> ACP bridge suite (dedicated phase)
|
||||
|
||||
Inspector ACP suite requirements:
|
||||
|
||||
1. Must run through real browser automation with `agent-browser` against `/ui/`.
|
||||
Current script: `frontend/packages/inspector/tests/agent-browser.e2e.sh`.
|
||||
2. Must not rely only on mocked component tests for pass criteria.
|
||||
3. Must cover one simple flow: spawn agent/session, send message, verify response renders.
|
||||
4. Must run in CI and block phase progression on failures.
|
||||
|
||||
## Phase 1: Teardown
|
||||
|
||||
1. Delete in-house protocol crates and docs listed in `research/acp/00-delete-first.md`.
|
||||
2. Remove workspace dependencies on deleted crates from `Cargo.toml`.
|
||||
3. Remove all `/v1` route registration and mount a unified `/v1/*` removed-handler (HTTP 410 + `application/problem+json`).
|
||||
4. Remove/disable CLI `api` commands that target `/v1`.
|
||||
5. Comment out/disable `/opencode/*` compat routes during ACP core bring-up.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Project builds with v1 protocol code removed.
|
||||
- No references to `sandbox-agent-universal-agent-schema` remain.
|
||||
- Any `/v1/*` request returns explicit "v1 removed" error.
|
||||
- `/opencode/*` is disabled (known broken) until Phase 7.
|
||||
|
||||
Validation gate:
|
||||
|
||||
- Build/test sanity after teardown compiles cleanly.
|
||||
- Static checks confirm removed modules/types are no longer referenced.
|
||||
- `/v1/*` returns HTTP 410 + stable error payload.
|
||||
- `/opencode/*` returns disabled/unavailable response.
|
||||
|
||||
## Phase 2: ACP Core Runtime
|
||||
|
||||
1. Add ACP transport module in server package (`acp_runtime.rs` + router integration).
|
||||
2. Implement agent process process manager (spawn, supervise, reconnect policy).
|
||||
3. Implement JSON-RPC bridge: HTTP POST/SSE <-> agent process stdio.
|
||||
4. Add connection registry keyed by `X-ACP-Connection-Id`.
|
||||
5. Include unstable ACP methods in the v2 profile (`session/list`, `session/fork`, `session/resume`, `session/set_model`, `$/cancel_request`).
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- End-to-end `initialize`, `session/new`, `session/prompt` works through one agent process.
|
||||
|
||||
Validation gate:
|
||||
|
||||
- End-to-end ACP flow test over `/v2/rpc` (request/response + streamed notifications).
|
||||
- Cancellation test (`session/cancel`) with proper terminal response behavior.
|
||||
- HITL request/response round-trip test (`session/request_permission` path).
|
||||
- SSE ordering and reconnection behavior test (`Last-Event-ID` replay path).
|
||||
- Explicit close test (`DELETE /v2/rpc`) including idempotent double-close behavior.
|
||||
- Unstable ACP methods validation (`session/list`, `session/fork`, `session/resume`, `session/set_model`, `$/cancel_request`) for agent processes that advertise support.
|
||||
|
||||
## Phase 3: Installer Refactor
|
||||
|
||||
1. Replace agent-specific spawn contracts in `server/packages/agent-management/src/agents.rs` with agent process-centric spawn.
|
||||
2. Add agent process install manifests and downloader logic.
|
||||
3. Keep native agent install where agent process depends on local CLI.
|
||||
4. Add install verification command per agent process.
|
||||
5. Add ACP registry integration for install metadata + fallback sources.
|
||||
6. Generate install instructions from manifest and expose provenance (`registry` or `fallback`) in API/CLI.
|
||||
7. Implement lazy install path on first `/v2/rpc` initialize (with per-agent install lock and idempotent results).
|
||||
8. Add config to disable lazy install for preprovisioned environments.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `install` provisions both required binaries (agent + agent process) for supported agents.
|
||||
|
||||
Validation gate:
|
||||
|
||||
- Explicit install command tests for each supported agent.
|
||||
- Lazy install on first ACP `initialize` test.
|
||||
- Reinstall/version/provenance assertions.
|
||||
|
||||
## Phase 4: v2 HTTP API
|
||||
|
||||
1. Mount `/v2/rpc` POST and SSE endpoints.
|
||||
2. Add `/v2/health`, `/v2/agents`, `/v2/agents/{agent}/install`.
|
||||
3. Add auth integration on connection lifecycle.
|
||||
4. Keep `/ui/` inspector route and migrate inspector backend calls to ACP v2 transport.
|
||||
5. Remove v1 OpenAPI generation from default docs build.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- v2 endpoints documented and passing integration tests.
|
||||
|
||||
Validation gate:
|
||||
|
||||
- Contract tests for all `/v2` endpoints (`/v2/rpc`, `/v2/health`, `/v2/agents`, install).
|
||||
- Auth tests (valid, missing, invalid token).
|
||||
- Error mapping tests (bad envelope, unknown connection, timeout paths).
|
||||
- `/v1/*` removal contract test (HTTP 410 + stable payload).
|
||||
- Inspector ACP `agent-browser` flow tests pass.
|
||||
- `DELETE /v2/rpc` close contract tests pass.
|
||||
|
||||
## Phase 5: SDK and CLI v2
|
||||
|
||||
1. Add ACP transport client in `sdks/typescript` by embedding `@agentclientprotocol/sdk` (no in-house ACP reimplementation).
|
||||
2. Implement custom ACP-over-HTTP transport agent process in our SDK (official ACP client SDK does not provide required Streamable HTTP behavior out of the box).
|
||||
3. Add inspector frontend client wiring to use ACP-over-HTTP transport primitives.
|
||||
4. Add CLI commands for sending raw ACP envelopes and streaming ACP messages.
|
||||
5. Remove v1-only SDK/CLI methods (or hard-fail with "v1 removed").
|
||||
6. Regenerate docs to v2 ACP contract.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- SDK can complete a full ACP prompt turn over `/v2/rpc`.
|
||||
|
||||
Validation gate:
|
||||
|
||||
- TypeScript SDK end-to-end tests in both embedded and server modes.
|
||||
- Parity tests ensuring SDK uses upstream ACP SDK primitives (no duplicate protocol stack).
|
||||
- Inspector end-to-end `agent-browser` tests using ACP-over-HTTP transport.
|
||||
|
||||
## Phase 6: Test and Rollout
|
||||
|
||||
1. Replace v1 HTTP/session tests with ACP transport contract tests.
|
||||
2. Add smoke tests per supported agent process.
|
||||
Current deterministic matrix: `server/packages/sandbox-agent/tests/v2_agent_process_matrix.rs`.
|
||||
3. Add canary rollout notes directly in `docs/quickstart.mdx`, `docs/cli.mdx`, and `docs/sdks/typescript.mdx`.
|
||||
4. Update docs for v2 ACP, `/v1/*` removal, inspector ACP behavior, and SDK usage.
|
||||
5. Keep v1 endpoints hard-removed (`410`) until/unless a separate compatibility project is approved.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- CI is green on ACP-native tests only.
|
||||
|
||||
Validation gate:
|
||||
|
||||
- Full matrix run across all supported agent processes.
|
||||
- Smoke tests for install + prompt + stream for each supported agent process.
|
||||
- Inspector `agent-browser` suite passes in CI for ACP mode (`.github/workflows/ci.yaml`).
|
||||
- Docs updates are published with the rollout.
|
||||
|
||||
## Phase 7: OpenCode <-> ACP Bridge (Dedicated Step)
|
||||
|
||||
1. Keep `/opencode/*` commented out/disabled through Phases 1-6.
|
||||
2. Implement OpenCode <-> ACP bridge on top of v2 ACP runtime.
|
||||
3. Re-enable `server/packages/sandbox-agent/src/opencode_compat.rs` routes/tests at full capability.
|
||||
4. Add dedicated integration tests that validate OpenCode SDK/TUI flows through ACP v2 internals.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- OpenCode compatibility works through ACP internals (no fallback to legacy in-house protocol).
|
||||
|
||||
Validation gate:
|
||||
|
||||
- OpenCode compatibility suite passes against ACP-backed implementation.
|
||||
- Regression tests ensure no dependency on removed in-house protocol runtime.
|
||||
|
||||
## Compatibility Layer (optional future project)
|
||||
|
||||
1. No compatibility layer is in the current v2 scope.
|
||||
2. If later approved, it should be a separate project with a dedicated spec and test matrix.
|
||||
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 |
|
||||
387
research/acp/missing-features-spec/04-filesystem-api.md
Normal file
387
research/acp/missing-features-spec/04-filesystem-api.md
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
# Feature 4: Filesystem API
|
||||
|
||||
**Implementation approach:** Custom HTTP endpoints (not ACP), per CLAUDE.md
|
||||
|
||||
## Summary
|
||||
|
||||
v1 had 8 filesystem endpoints. v2 has only ACP `fs/read_text_file` + `fs/write_text_file` (text-only, agent->client direction). The full filesystem API should be re-implemented as Sandbox Agent-specific HTTP contracts at `/v2/fs/*`.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
- ACP stable: `fs/read_text_file`, `fs/write_text_file` (client methods invoked by agents, text-only)
|
||||
- No HTTP filesystem endpoints exist in current `router.rs`
|
||||
- `rfds-vs-extensions.md` confirms: "Already extension (`/v2/fs/*` custom HTTP surface)"
|
||||
- CLAUDE.md: "Filesystem and terminal APIs remain Sandbox Agent-specific HTTP contracts and are not ACP"
|
||||
|
||||
## v1 Reference (source commit)
|
||||
|
||||
Port behavior from commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`.
|
||||
|
||||
## v1 Endpoints (from `router.rs`)
|
||||
|
||||
| Method | Path | Handler | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| GET | `/v1/fs/entries` | `fs_entries` | List directory entries |
|
||||
| GET | `/v1/fs/file` | `fs_read_file` | Read file raw bytes |
|
||||
| PUT | `/v1/fs/file` | `fs_write_file` | Write file raw bytes |
|
||||
| DELETE | `/v1/fs/entry` | `fs_delete_entry` | Delete file or directory |
|
||||
| POST | `/v1/fs/mkdir` | `fs_mkdir` | Create directory |
|
||||
| POST | `/v1/fs/move` | `fs_move` | Move/rename file or directory |
|
||||
| GET | `/v1/fs/stat` | `fs_stat` | Get file/directory metadata |
|
||||
| POST | `/v1/fs/upload-batch` | `fs_upload_batch` | Upload tar archive |
|
||||
|
||||
## v1 Types (exact, from `router.rs`)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsPathQuery {
|
||||
pub path: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", alias = "session_id")]
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsEntriesQuery {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", alias = "session_id")]
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsSessionQuery {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", alias = "session_id")]
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsDeleteQuery {
|
||||
pub path: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", alias = "session_id")]
|
||||
pub session_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub recursive: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsUploadBatchQuery {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", alias = "session_id")]
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FsEntryType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsEntry {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub entry_type: FsEntryType,
|
||||
pub size: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub modified: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsStat {
|
||||
pub path: String,
|
||||
pub entry_type: FsEntryType,
|
||||
pub size: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub modified: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsWriteResponse {
|
||||
pub path: String,
|
||||
pub bytes_written: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsMoveRequest {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub overwrite: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsMoveResponse {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsActionResponse {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsUploadBatchResponse {
|
||||
pub paths: Vec<String>,
|
||||
pub truncated: bool,
|
||||
}
|
||||
```
|
||||
|
||||
## v1 Handler Implementations (exact, from `router.rs`)
|
||||
|
||||
### `fs_entries` (GET /v1/fs/entries)
|
||||
|
||||
```rust
|
||||
async fn fs_entries(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<FsEntriesQuery>,
|
||||
) -> Result<Json<Vec<FsEntry>>, ApiError> {
|
||||
let path = query.path.unwrap_or_else(|| ".".to_string());
|
||||
let target = resolve_fs_path(&state, query.session_id.as_deref(), &path).await?;
|
||||
let metadata = fs::metadata(&target).map_err(|err| map_fs_error(&target, err))?;
|
||||
if !metadata.is_dir() {
|
||||
return Err(SandboxError::InvalidRequest {
|
||||
message: format!("path is not a directory: {}", target.display()),
|
||||
}.into());
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
for entry in fs::read_dir(&target).map_err(|err| map_fs_error(&target, err))? {
|
||||
let entry = entry.map_err(|err| SandboxError::StreamError { message: err.to_string() })?;
|
||||
let path = entry.path();
|
||||
let metadata = entry.metadata().map_err(|err| SandboxError::StreamError { message: err.to_string() })?;
|
||||
let entry_type = if metadata.is_dir() { FsEntryType::Directory } else { FsEntryType::File };
|
||||
let modified = metadata.modified().ok().and_then(|time| {
|
||||
chrono::DateTime::<chrono::Utc>::from(time).to_rfc3339().into()
|
||||
});
|
||||
entries.push(FsEntry {
|
||||
name: entry.file_name().to_string_lossy().to_string(),
|
||||
path: path.to_string_lossy().to_string(),
|
||||
entry_type, size: metadata.len(), modified,
|
||||
});
|
||||
}
|
||||
Ok(Json(entries))
|
||||
}
|
||||
```
|
||||
|
||||
### `fs_read_file` (GET /v1/fs/file)
|
||||
|
||||
```rust
|
||||
async fn fs_read_file(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<FsPathQuery>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let target = resolve_fs_path(&state, query.session_id.as_deref(), &query.path).await?;
|
||||
let metadata = fs::metadata(&target).map_err(|err| map_fs_error(&target, err))?;
|
||||
if !metadata.is_file() {
|
||||
return Err(SandboxError::InvalidRequest {
|
||||
message: format!("path is not a file: {}", target.display()),
|
||||
}.into());
|
||||
}
|
||||
let bytes = fs::read(&target).map_err(|err| map_fs_error(&target, err))?;
|
||||
Ok(([(header::CONTENT_TYPE, "application/octet-stream")], Bytes::from(bytes)).into_response())
|
||||
}
|
||||
```
|
||||
|
||||
### `fs_write_file` (PUT /v1/fs/file)
|
||||
|
||||
```rust
|
||||
async fn fs_write_file(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<FsPathQuery>,
|
||||
body: Bytes,
|
||||
) -> Result<Json<FsWriteResponse>, ApiError> {
|
||||
let target = resolve_fs_path(&state, query.session_id.as_deref(), &query.path).await?;
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent).map_err(|err| map_fs_error(parent, err))?;
|
||||
}
|
||||
fs::write(&target, &body).map_err(|err| map_fs_error(&target, err))?;
|
||||
Ok(Json(FsWriteResponse {
|
||||
path: target.to_string_lossy().to_string(),
|
||||
bytes_written: body.len() as u64,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### `fs_delete_entry` (DELETE /v1/fs/entry)
|
||||
|
||||
```rust
|
||||
async fn fs_delete_entry(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<FsDeleteQuery>,
|
||||
) -> Result<Json<FsActionResponse>, ApiError> {
|
||||
let target = resolve_fs_path(&state, query.session_id.as_deref(), &query.path).await?;
|
||||
let metadata = fs::metadata(&target).map_err(|err| map_fs_error(&target, err))?;
|
||||
if metadata.is_dir() {
|
||||
if query.recursive.unwrap_or(false) {
|
||||
fs::remove_dir_all(&target).map_err(|err| map_fs_error(&target, err))?;
|
||||
} else {
|
||||
fs::remove_dir(&target).map_err(|err| map_fs_error(&target, err))?;
|
||||
}
|
||||
} else {
|
||||
fs::remove_file(&target).map_err(|err| map_fs_error(&target, err))?;
|
||||
}
|
||||
Ok(Json(FsActionResponse { path: target.to_string_lossy().to_string() }))
|
||||
}
|
||||
```
|
||||
|
||||
### `fs_mkdir` (POST /v1/fs/mkdir)
|
||||
|
||||
```rust
|
||||
async fn fs_mkdir(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<FsPathQuery>,
|
||||
) -> Result<Json<FsActionResponse>, ApiError> {
|
||||
let target = resolve_fs_path(&state, query.session_id.as_deref(), &query.path).await?;
|
||||
fs::create_dir_all(&target).map_err(|err| map_fs_error(&target, err))?;
|
||||
Ok(Json(FsActionResponse { path: target.to_string_lossy().to_string() }))
|
||||
}
|
||||
```
|
||||
|
||||
### `fs_move` (POST /v1/fs/move)
|
||||
|
||||
```rust
|
||||
async fn fs_move(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<FsSessionQuery>,
|
||||
Json(request): Json<FsMoveRequest>,
|
||||
) -> Result<Json<FsMoveResponse>, ApiError> {
|
||||
let session_id = query.session_id.as_deref();
|
||||
let from = resolve_fs_path(&state, session_id, &request.from).await?;
|
||||
let to = resolve_fs_path(&state, session_id, &request.to).await?;
|
||||
if to.exists() {
|
||||
if request.overwrite.unwrap_or(false) {
|
||||
let metadata = fs::metadata(&to).map_err(|err| map_fs_error(&to, err))?;
|
||||
if metadata.is_dir() {
|
||||
fs::remove_dir_all(&to).map_err(|err| map_fs_error(&to, err))?;
|
||||
} else {
|
||||
fs::remove_file(&to).map_err(|err| map_fs_error(&to, err))?;
|
||||
}
|
||||
} else {
|
||||
return Err(SandboxError::InvalidRequest {
|
||||
message: format!("destination already exists: {}", to.display()),
|
||||
}.into());
|
||||
}
|
||||
}
|
||||
if let Some(parent) = to.parent() {
|
||||
fs::create_dir_all(parent).map_err(|err| map_fs_error(parent, err))?;
|
||||
}
|
||||
fs::rename(&from, &to).map_err(|err| map_fs_error(&from, err))?;
|
||||
Ok(Json(FsMoveResponse {
|
||||
from: from.to_string_lossy().to_string(),
|
||||
to: to.to_string_lossy().to_string(),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### `fs_stat` (GET /v1/fs/stat)
|
||||
|
||||
```rust
|
||||
async fn fs_stat(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<FsPathQuery>,
|
||||
) -> Result<Json<FsStat>, ApiError> {
|
||||
let target = resolve_fs_path(&state, query.session_id.as_deref(), &query.path).await?;
|
||||
let metadata = fs::metadata(&target).map_err(|err| map_fs_error(&target, err))?;
|
||||
let entry_type = if metadata.is_dir() { FsEntryType::Directory } else { FsEntryType::File };
|
||||
let modified = metadata.modified().ok().and_then(|time| {
|
||||
chrono::DateTime::<chrono::Utc>::from(time).to_rfc3339().into()
|
||||
});
|
||||
Ok(Json(FsStat {
|
||||
path: target.to_string_lossy().to_string(),
|
||||
entry_type, size: metadata.len(), modified,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### `fs_upload_batch` (POST /v1/fs/upload-batch)
|
||||
|
||||
```rust
|
||||
async fn fs_upload_batch(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<FsUploadBatchQuery>,
|
||||
body: Bytes,
|
||||
) -> Result<Json<FsUploadBatchResponse>, ApiError> {
|
||||
let content_type = headers.get(header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok()).unwrap_or_default();
|
||||
if !content_type.starts_with("application/x-tar") {
|
||||
return Err(SandboxError::InvalidRequest {
|
||||
message: "content-type must be application/x-tar".to_string(),
|
||||
}.into());
|
||||
}
|
||||
let path = query.path.unwrap_or_else(|| ".".to_string());
|
||||
let base = resolve_fs_path(&state, query.session_id.as_deref(), &path).await?;
|
||||
fs::create_dir_all(&base).map_err(|err| map_fs_error(&base, err))?;
|
||||
|
||||
let mut archive = Archive::new(Cursor::new(body));
|
||||
let mut extracted = Vec::new();
|
||||
let mut truncated = false;
|
||||
for entry in archive.entries().map_err(|err| SandboxError::StreamError { message: err.to_string() })? {
|
||||
let mut entry = entry.map_err(|err| SandboxError::StreamError { message: err.to_string() })?;
|
||||
let entry_path = entry.path().map_err(|err| SandboxError::StreamError { message: err.to_string() })?;
|
||||
let clean_path = sanitize_relative_path(&entry_path)?;
|
||||
if clean_path.as_os_str().is_empty() { continue; }
|
||||
let dest = base.join(&clean_path);
|
||||
if !dest.starts_with(&base) {
|
||||
return Err(SandboxError::InvalidRequest {
|
||||
message: format!("tar entry escapes destination: {}", entry_path.display()),
|
||||
}.into());
|
||||
}
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent).map_err(|err| map_fs_error(parent, err))?;
|
||||
}
|
||||
entry.unpack(&dest).map_err(|err| SandboxError::StreamError { message: err.to_string() })?;
|
||||
if extracted.len() < 1024 {
|
||||
extracted.push(dest.to_string_lossy().to_string());
|
||||
} else { truncated = true; }
|
||||
}
|
||||
|
||||
Ok(Json(FsUploadBatchResponse { paths: extracted, truncated }))
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### New v2 Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/v2/fs/entries` | List directory entries |
|
||||
| GET | `/v2/fs/file` | Read file raw bytes |
|
||||
| PUT | `/v2/fs/file` | Write file raw bytes |
|
||||
| DELETE | `/v2/fs/entry` | Delete file or directory |
|
||||
| POST | `/v2/fs/mkdir` | Create directory |
|
||||
| POST | `/v2/fs/move` | Move/rename |
|
||||
| GET | `/v2/fs/stat` | File metadata |
|
||||
| POST | `/v2/fs/upload-batch` | Upload tar archive |
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/router.rs` | Add all 8 `/v2/fs/*` endpoints with handlers (port from v1 with v2 path prefix) |
|
||||
| `server/packages/sandbox-agent/src/cli.rs` | Add CLI `fs` subcommands (list, read, write, delete, mkdir, move, stat) |
|
||||
| `sdks/typescript/src/client.ts` | Add filesystem methods to SDK |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add filesystem endpoint tests |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/openapi.json` | Add `/v2/fs/*` endpoint specs |
|
||||
| `docs/cli.mdx` | Add `fs` subcommand documentation |
|
||||
| `docs/sdks/typescript.mdx` | Document filesystem SDK methods |
|
||||
90
research/acp/missing-features-spec/05-health-endpoint.md
Normal file
90
research/acp/missing-features-spec/05-health-endpoint.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Feature 5: Health Endpoint
|
||||
|
||||
**Implementation approach:** Enhance existing `GET /v2/health`
|
||||
|
||||
## Summary
|
||||
|
||||
v1 had a typed `HealthResponse` with detailed status. v2 `GET /v2/health` exists but returns only `{ status: "ok", api_version: "v2" }`. Needs enrichment.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
From `router.rs:332-346`:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct HealthResponse {
|
||||
pub status: String,
|
||||
pub api_version: String,
|
||||
}
|
||||
|
||||
async fn get_v2_health() -> Json<HealthResponse> {
|
||||
Json(HealthResponse {
|
||||
status: "ok".to_string(),
|
||||
api_version: "v2".to_string(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## v1 Reference (source commit)
|
||||
|
||||
Port behavior from commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`.
|
||||
|
||||
## v1 Health Response
|
||||
|
||||
v1 returned a richer health response:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Serialize, JsonSchema, ToSchema)]
|
||||
pub struct HealthResponse {
|
||||
pub status: HealthStatus,
|
||||
pub version: String,
|
||||
pub uptime_ms: u64,
|
||||
pub agents: Vec<AgentHealthInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HealthStatus {
|
||||
Healthy,
|
||||
Degraded,
|
||||
Unhealthy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema, ToSchema)]
|
||||
pub struct AgentHealthInfo {
|
||||
pub agent: String,
|
||||
pub installed: bool,
|
||||
pub running: bool,
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### v1-Parity HealthResponse
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct HealthResponse {
|
||||
pub status: HealthStatus,
|
||||
pub version: String,
|
||||
pub uptime_ms: u64,
|
||||
pub agents: Vec<AgentHealthInfo>,
|
||||
}
|
||||
```
|
||||
|
||||
`GET /v2/health` should mirror v1 semantics and response shape (ported from commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`), while keeping the v2 route path.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/router.rs` | Port v1 health response types/logic onto `GET /v2/health` |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Update health endpoint test for full v1-parity payload |
|
||||
| `sdks/typescript/src/client.ts` | Update `HealthResponse` type |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/openapi.json` | Update `/v2/health` response schema |
|
||||
| `docs/sdks/typescript.mdx` | Document enriched health response |
|
||||
144
research/acp/missing-features-spec/06-server-status.md
Normal file
144
research/acp/missing-features-spec/06-server-status.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Feature 6: Server Status
|
||||
|
||||
**Implementation approach:** Extension fields on `GET /v2/agents` and `GET /v2/health`
|
||||
|
||||
## Summary
|
||||
|
||||
v1 had `ServerStatus` (Running/Stopped/Error) and `ServerStatusInfo` (baseUrl, lastError, restartCount, uptimeMs) per agent. v2 has none of this. Add server/agent process status tracking.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
`GET /v2/agents` returns `AgentInfo` with install state only:
|
||||
|
||||
```rust
|
||||
pub struct AgentInfo {
|
||||
pub id: String,
|
||||
pub native_required: bool,
|
||||
pub native_installed: bool,
|
||||
pub native_version: Option<String>,
|
||||
pub agent_process_installed: bool,
|
||||
pub agent_process_source: Option<String>,
|
||||
pub agent_process_version: Option<String>,
|
||||
pub capabilities: AgentCapabilities,
|
||||
}
|
||||
```
|
||||
|
||||
No runtime status (running/stopped/error), no error tracking, no restart counts.
|
||||
|
||||
## v1 Types (exact, from `router.rs`)
|
||||
|
||||
```rust
|
||||
/// Status of a shared server process for an agent
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ServerStatus {
|
||||
/// Server is running and accepting requests
|
||||
Running,
|
||||
/// Server is not currently running
|
||||
Stopped,
|
||||
/// Server is running but unhealthy
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerStatusInfo {
|
||||
pub status: ServerStatus,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_url: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub uptime_ms: Option<u64>,
|
||||
pub restart_count: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_error: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## v1 Implementation (exact)
|
||||
|
||||
### `ManagedServer::status_info`
|
||||
|
||||
```rust
|
||||
fn status_info(&self) -> ServerStatusInfo {
|
||||
let uptime_ms = self.start_time
|
||||
.map(|started| started.elapsed().as_millis() as u64);
|
||||
ServerStatusInfo {
|
||||
status: self.status.clone(),
|
||||
base_url: self.base_url(),
|
||||
uptime_ms,
|
||||
restart_count: self.restart_count,
|
||||
last_error: self.last_error.clone(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `AgentServerManager::status_snapshot`
|
||||
|
||||
```rust
|
||||
async fn status_snapshot(&self) -> HashMap<AgentId, ServerStatusInfo> {
|
||||
let servers = self.servers.lock().await;
|
||||
servers.iter()
|
||||
.map(|(agent, server)| (*agent, server.status_info()))
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
|
||||
### `AgentServerManager::update_server_error`
|
||||
|
||||
```rust
|
||||
async fn update_server_error(&self, agent: AgentId, message: String) {
|
||||
let mut servers = self.servers.lock().await;
|
||||
if let Some(server) = servers.get_mut(&agent) {
|
||||
server.status = ServerStatus::Error;
|
||||
server.start_time = None;
|
||||
server.last_error = Some(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### ACP Runtime Tracking
|
||||
|
||||
The `AcpRuntime` needs to track per-agent backend process:
|
||||
|
||||
```rust
|
||||
struct AgentProcessStatus {
|
||||
status: String, // "running" | "stopped" | "error"
|
||||
start_time: Option<Instant>,
|
||||
restart_count: u64,
|
||||
last_error: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
Track:
|
||||
- Process start → set status to "running", record `start_time`, increment `restart_count`
|
||||
- Process exit (normal) → set status to "stopped", clear `start_time`
|
||||
- Process exit (error) → set status to "error", record `last_error`, clear `start_time`
|
||||
|
||||
### Add to AgentInfo
|
||||
|
||||
```rust
|
||||
pub struct AgentInfo {
|
||||
// ... existing fields ...
|
||||
pub server_status: Option<ServerStatusInfo>,
|
||||
}
|
||||
```
|
||||
|
||||
Only include `server_status` for agents that use shared processes (Codex, OpenCode).
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Track agent process lifecycle (start/stop/error/restart count) per `AgentId`; expose `status_snapshot()` method |
|
||||
| `server/packages/sandbox-agent/src/router.rs` | Add `ServerStatus`, `ServerStatusInfo` types; add `server_status` to `AgentInfo`; query runtime for status in `get_v2_agents` |
|
||||
| `sdks/typescript/src/client.ts` | Update `AgentInfo` type with `serverStatus` |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Test server status in agent listing |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/openapi.json` | Update `/v2/agents` response with `server_status` |
|
||||
| `docs/sdks/typescript.mdx` | Document `serverStatus` field |
|
||||
123
research/acp/missing-features-spec/07-session-termination.md
Normal file
123
research/acp/missing-features-spec/07-session-termination.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Feature 7: Session Termination
|
||||
|
||||
**Implementation approach:** ACP extension, referencing existing ACP RFD
|
||||
|
||||
## Summary
|
||||
|
||||
v1 had explicit session termination (`POST /v1/sessions/{id}/terminate`). v2 only has `session/cancel` (turn cancellation, not session kill) and `DELETE /v2/rpc` (connection close, not session termination). Need explicit session destroy/terminate semantics.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
- `session/cancel` — cancels an in-flight prompt turn only
|
||||
- `DELETE /v2/rpc` — closes the HTTP connection, does **not** terminate the session
|
||||
- `_sandboxagent/session/detach` — detaches a session from a connection (multi-client visibility)
|
||||
- No session termination/deletion exists
|
||||
- `rfds-vs-extensions.md`: "Session Termination: Not covered by ACP. Only implement if product explicitly requires termination semantics beyond session/cancel"
|
||||
- `extensibility-status.md`: Documents `_sandboxagent/session/terminate` as proposed but not implemented
|
||||
|
||||
## v1 Implementation
|
||||
|
||||
### HTTP Endpoint
|
||||
|
||||
```
|
||||
POST /v1/sessions/{id}/terminate
|
||||
```
|
||||
|
||||
### Handler (from `router.rs`)
|
||||
|
||||
The terminate handler:
|
||||
1. Looked up the session by ID
|
||||
2. Killed the agent subprocess (SIGTERM then SIGKILL after grace period)
|
||||
3. Emitted a `session.ended` event with `reason: Terminated, terminated_by: Daemon`
|
||||
4. Cleaned up session state
|
||||
|
||||
### v1 Types
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct SessionEndedData {
|
||||
pub reason: SessionEndReason,
|
||||
pub terminated_by: TerminatedBy,
|
||||
pub message: Option<String>,
|
||||
pub exit_code: Option<i32>,
|
||||
pub stderr: Option<StderrOutput>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SessionEndReason {
|
||||
Completed,
|
||||
Error,
|
||||
Terminated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TerminatedBy {
|
||||
Agent,
|
||||
Daemon,
|
||||
}
|
||||
```
|
||||
|
||||
## ACP RFD Reference
|
||||
|
||||
Per `~/misc/acp-docs/`, session termination is listed as an RFD topic. The existing ACP spec does not define a `session/terminate` or `session/delete` method.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### ACP Extension Method
|
||||
|
||||
```
|
||||
_sandboxagent/session/terminate
|
||||
```
|
||||
|
||||
Client -> Runtime request:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "t-1",
|
||||
"method": "_sandboxagent/session/terminate",
|
||||
"params": {
|
||||
"sessionId": "session-uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "t-1",
|
||||
"result": {
|
||||
"terminated": true,
|
||||
"reason": "terminated",
|
||||
"terminatedBy": "daemon"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
1. Client sends `_sandboxagent/session/terminate` request
|
||||
2. Runtime identifies the session and its owning agent process
|
||||
3. For shared-process agents (Codex, OpenCode): send a cancel/terminate signal to the agent process for that specific session
|
||||
4. For per-turn subprocess agents (Claude, Amp): kill the subprocess if running, mark session as terminated
|
||||
5. Emit `_sandboxagent/session/ended` to all connected clients watching that session
|
||||
6. Method is idempotent: repeated calls on an already-ended session return success without side effects
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add `_sandboxagent/session/terminate` handler; add session removal from registry; add process kill logic |
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock terminate support |
|
||||
| `sdks/typescript/src/client.ts` | Add `terminateSession(sessionId)` method |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add session termination test |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/sdks/typescript.mdx` | Document `terminateSession` method |
|
||||
| `research/acp/spec.md` | Add `_sandboxagent/session/terminate` to extension methods list |
|
||||
| `research/acp/rfds-vs-extensions.md` | Update session termination row |
|
||||
130
research/acp/missing-features-spec/08-model-variants.md
Normal file
130
research/acp/missing-features-spec/08-model-variants.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Feature 8: Model Variants
|
||||
|
||||
> **Status:** Deferred / out of scope for the current implementation pass.
|
||||
|
||||
**Implementation approach:** Enhance existing `_sandboxagent/session/list_models` extension
|
||||
|
||||
## Summary
|
||||
|
||||
v1 had `AgentModelInfo.variants`, `AgentModelInfo.defaultVariant`, and `CreateSessionRequest.variant`. v2 already has `_sandboxagent/session/list_models` but the variant fields need to be verified and the session-creation variant selection needs to work end-to-end.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
From `acp_runtime/mod.rs`, `_sandboxagent/session/list_models` is implemented and returns:
|
||||
- `availableModels[]` with `modelId`, `name`, `description`
|
||||
- `currentModelId`
|
||||
- Fields for `defaultVariant`, `variants[]` are documented in `rfds-vs-extensions.md`
|
||||
|
||||
From v1 `router.rs`, model/variant types existed:
|
||||
|
||||
```rust
|
||||
pub struct AgentModelsResponse {
|
||||
pub models: Vec<AgentModelInfo>,
|
||||
pub default_model: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AgentModelInfo {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub variants: Option<Vec<AgentModelVariant>>,
|
||||
pub default_variant: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AgentModelVariant {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## v1 Usage
|
||||
|
||||
### Pre-session Model Discovery
|
||||
|
||||
```
|
||||
GET /v1/agents/{agent}/models
|
||||
```
|
||||
|
||||
Returned `AgentModelsResponse` with full model list including variants.
|
||||
|
||||
### Session Creation with Variant
|
||||
|
||||
```
|
||||
POST /v1/sessions
|
||||
```
|
||||
|
||||
Body included `variant: Option<String>` to select a specific model variant at session creation time.
|
||||
|
||||
### Per-Agent Model Logic (from `router.rs`)
|
||||
|
||||
```rust
|
||||
fn amp_models_response() -> AgentModelsResponse {
|
||||
AgentModelsResponse {
|
||||
models: vec![AgentModelInfo {
|
||||
id: "amp-default".to_string(),
|
||||
name: Some("Amp Default".to_string()),
|
||||
variants: None,
|
||||
default_variant: None,
|
||||
}],
|
||||
default_model: Some("amp-default".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn mock_models_response() -> AgentModelsResponse {
|
||||
AgentModelsResponse {
|
||||
models: vec![AgentModelInfo {
|
||||
id: "mock".to_string(),
|
||||
name: Some("Mock".to_string()),
|
||||
variants: None,
|
||||
default_variant: None,
|
||||
}],
|
||||
default_model: Some("mock".to_string()),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Claude and Codex models were fetched dynamically from the agent process.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Verify/Enrich `_sandboxagent/session/list_models`
|
||||
|
||||
The existing extension method already returns model data. Verify that:
|
||||
|
||||
1. `variants` array is included in each model entry when available
|
||||
2. `defaultVariant` is included when available
|
||||
3. The response shape matches the documented RFD shape
|
||||
|
||||
### Add Variant to Session Creation
|
||||
|
||||
Session creation via `session/new` should accept a variant hint in `_meta`:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "session/new",
|
||||
"params": {
|
||||
"_meta": {
|
||||
"sandboxagent.dev": {
|
||||
"variant": "opus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The runtime should forward this variant to the agent process (e.g., as a model parameter in the spawn command or via `session/set_model`).
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Verify `list_models` response includes `variants`/`defaultVariant`; extract and forward `variant` from `session/new` `_meta` |
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add variant support to mock model listing |
|
||||
| `sdks/typescript/src/client.ts` | Update `listModels` return type to include variants |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add model variants test |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/sdks/typescript.mdx` | Document variant support in model listing and session creation |
|
||||
| `research/acp/spec.md` | Update `_sandboxagent/session/list_models` payload shape |
|
||||
98
research/acp/missing-features-spec/10-include-raw.md
Normal file
98
research/acp/missing-features-spec/10-include-raw.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Feature 10: `include_raw`
|
||||
|
||||
> **Status:** Deferred / out of scope for the current implementation pass.
|
||||
|
||||
**Implementation approach:** ACP extension
|
||||
|
||||
## Summary
|
||||
|
||||
v1 had an `include_raw` option that preserved the original agent JSON alongside normalized events. The `UniversalEvent.raw` field held the verbatim agent output. v2 has `_sandboxagent/agent/unparsed` for parse errors but no mechanism for clients to request raw agent payloads alongside normalized ACP events.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
- `_sandboxagent/agent/unparsed` — sends notifications when the runtime fails to parse agent output (error recovery only)
|
||||
- No option for clients to request raw agent JSON alongside normal ACP events
|
||||
- ACP events are already the agent's native JSON-RPC output (for agents that speak ACP natively); the "raw" concept is less meaningful when the agent already speaks ACP
|
||||
|
||||
## v1 Types
|
||||
|
||||
```rust
|
||||
pub struct UniversalEvent {
|
||||
pub event_id: String,
|
||||
pub sequence: u64,
|
||||
pub time: String,
|
||||
pub session_id: String,
|
||||
pub native_session_id: Option<String>,
|
||||
pub synthetic: bool,
|
||||
pub source: EventSource,
|
||||
pub event_type: UniversalEventType,
|
||||
pub data: UniversalEventData,
|
||||
pub raw: Option<Value>, // <-- Raw agent output when include_raw=true
|
||||
}
|
||||
```
|
||||
|
||||
### v1 Usage
|
||||
|
||||
```
|
||||
GET /v1/sessions/{id}/events?include_raw=true
|
||||
```
|
||||
|
||||
When `include_raw=true`, each `UniversalEvent` included the verbatim JSON the agent process emitted before normalization into the universal schema.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Extension Design
|
||||
|
||||
Since v2 agents speak ACP natively (JSON-RPC), the "raw" concept changes:
|
||||
- For ACP-native agents: raw = the ACP JSON-RPC envelope itself (which clients already see)
|
||||
- For non-native agents or runtime-synthesized events: raw = the original agent output before transformation
|
||||
|
||||
The extension provides a way for clients to opt into receiving the pre-transformation payload.
|
||||
|
||||
### Opt-in via `_meta`
|
||||
|
||||
Client requests raw mode at connection initialization:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"_meta": {
|
||||
"sandboxagent.dev": {
|
||||
"includeRaw": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When enabled, notifications forwarded from the agent process include an additional `_meta.sandboxagent.dev.raw` field containing the original payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "session/update",
|
||||
"params": {
|
||||
// ... normalized ACP event ...
|
||||
"_meta": {
|
||||
"sandboxagent.dev": {
|
||||
"raw": { /* original agent JSON */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Track per-client `includeRaw` preference; attach raw payload to forwarded notifications when enabled |
|
||||
| `sdks/typescript/src/client.ts` | Add `includeRaw` option to connection config; expose raw data on event objects |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/sdks/typescript.mdx` | Document `includeRaw` option |
|
||||
| `research/acp/spec.md` | Document raw extension behavior |
|
||||
222
research/acp/missing-features-spec/12-agent-listing.md
Normal file
222
research/acp/missing-features-spec/12-agent-listing.md
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
# Feature 12: Agent Listing (Typed Response)
|
||||
|
||||
**Implementation approach:** Enhance existing `GET /v2/agents`
|
||||
|
||||
## Summary
|
||||
|
||||
v1 `GET /v1/agents` returned a typed `AgentListResponse` with `installed`, `credentialsAvailable`, `path`, `capabilities`, `serverStatus`. v2 `GET /v2/agents` returns a basic `AgentInfo` with only install state. Needs enrichment.
|
||||
|
||||
This feature also carries pre-session models/modes as optional fields when the agent is installed (Feature #13), rather than using separate model/mode endpoints.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
From `router.rs:265-275`:
|
||||
|
||||
```rust
|
||||
pub struct AgentInfo {
|
||||
pub id: String,
|
||||
pub native_required: bool,
|
||||
pub native_installed: bool,
|
||||
pub native_version: Option<String>,
|
||||
pub agent_process_installed: bool,
|
||||
pub agent_process_source: Option<String>,
|
||||
pub agent_process_version: Option<String>,
|
||||
pub capabilities: AgentCapabilities,
|
||||
}
|
||||
|
||||
pub struct AgentCapabilities {
|
||||
pub unstable_methods: bool,
|
||||
}
|
||||
```
|
||||
|
||||
## v1 Types (exact, from `router.rs`)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentCapabilities {
|
||||
pub plan_mode: bool,
|
||||
pub permissions: bool,
|
||||
pub questions: bool,
|
||||
pub tool_calls: bool,
|
||||
pub tool_results: bool,
|
||||
pub text_messages: bool,
|
||||
pub images: bool,
|
||||
pub file_attachments: bool,
|
||||
pub session_lifecycle: bool,
|
||||
pub error_events: bool,
|
||||
pub reasoning: bool,
|
||||
pub status: bool,
|
||||
pub command_execution: bool,
|
||||
pub file_changes: bool,
|
||||
pub mcp_tools: bool,
|
||||
pub streaming_deltas: bool,
|
||||
pub item_started: bool,
|
||||
/// Whether this agent uses a shared long-running server process (vs per-turn subprocess)
|
||||
pub shared_process: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentInfo {
|
||||
pub id: String,
|
||||
pub installed: bool,
|
||||
/// Whether the agent's required provider credentials are available
|
||||
pub credentials_available: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
pub capabilities: AgentCapabilities,
|
||||
/// Status of the shared server process (only present for agents with shared_process=true)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub server_status: Option<ServerStatusInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentListResponse {
|
||||
pub agents: Vec<AgentInfo>,
|
||||
}
|
||||
```
|
||||
|
||||
## v1 `list_agents` Handler (exact)
|
||||
|
||||
```rust
|
||||
async fn list_agents(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<AgentListResponse>, ApiError> {
|
||||
let manager = state.agent_manager.clone();
|
||||
let server_statuses = state.session_manager.server_manager.status_snapshot().await;
|
||||
|
||||
let agents = tokio::task::spawn_blocking(move || {
|
||||
let credentials = extract_all_credentials(&CredentialExtractionOptions::new());
|
||||
let has_anthropic = credentials.anthropic.is_some();
|
||||
let has_openai = credentials.openai.is_some();
|
||||
|
||||
all_agents().into_iter().map(|agent_id| {
|
||||
let installed = manager.is_installed(agent_id);
|
||||
let version = manager.version(agent_id).ok().flatten();
|
||||
let path = manager.resolve_binary(agent_id).ok();
|
||||
let capabilities = agent_capabilities_for(agent_id);
|
||||
|
||||
let credentials_available = match agent_id {
|
||||
AgentId::Claude | AgentId::Amp => has_anthropic,
|
||||
AgentId::Codex => has_openai,
|
||||
AgentId::Opencode => has_anthropic || has_openai,
|
||||
AgentId::Mock => true,
|
||||
};
|
||||
|
||||
let server_status = if capabilities.shared_process {
|
||||
Some(server_statuses.get(&agent_id).cloned().unwrap_or(
|
||||
ServerStatusInfo {
|
||||
status: ServerStatus::Stopped,
|
||||
base_url: None, uptime_ms: None,
|
||||
restart_count: 0, last_error: None,
|
||||
},
|
||||
))
|
||||
} else { None };
|
||||
|
||||
AgentInfo {
|
||||
id: agent_id.as_str().to_string(),
|
||||
installed, credentials_available, version,
|
||||
path: path.map(|p| p.to_string_lossy().to_string()),
|
||||
capabilities, server_status,
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}).await.map_err(|err| SandboxError::StreamError { message: err.to_string() })?;
|
||||
|
||||
Ok(Json(AgentListResponse { agents }))
|
||||
}
|
||||
```
|
||||
|
||||
## v1 Per-Agent Capability Mapping (exact)
|
||||
|
||||
```rust
|
||||
fn agent_capabilities_for(agent: AgentId) -> AgentCapabilities {
|
||||
match agent {
|
||||
AgentId::Claude => AgentCapabilities {
|
||||
plan_mode: false, permissions: true, questions: true,
|
||||
tool_calls: true, tool_results: true, text_messages: true,
|
||||
images: false, file_attachments: false, session_lifecycle: false,
|
||||
error_events: false, reasoning: false, status: false,
|
||||
command_execution: false, file_changes: false, mcp_tools: true,
|
||||
streaming_deltas: true, item_started: false, shared_process: false,
|
||||
},
|
||||
AgentId::Codex => AgentCapabilities {
|
||||
plan_mode: true, permissions: true, questions: false,
|
||||
tool_calls: true, tool_results: true, text_messages: true,
|
||||
images: true, file_attachments: true, session_lifecycle: true,
|
||||
error_events: true, reasoning: true, status: true,
|
||||
command_execution: true, file_changes: true, mcp_tools: true,
|
||||
streaming_deltas: true, item_started: true, shared_process: true,
|
||||
},
|
||||
AgentId::Opencode => AgentCapabilities {
|
||||
plan_mode: false, permissions: false, questions: false,
|
||||
tool_calls: true, tool_results: true, text_messages: true,
|
||||
images: true, file_attachments: true, session_lifecycle: true,
|
||||
error_events: true, reasoning: false, status: false,
|
||||
command_execution: false, file_changes: false, mcp_tools: true,
|
||||
streaming_deltas: true, item_started: true, shared_process: true,
|
||||
},
|
||||
AgentId::Amp => AgentCapabilities {
|
||||
plan_mode: false, permissions: false, questions: false,
|
||||
tool_calls: true, tool_results: true, text_messages: true,
|
||||
images: false, file_attachments: false, session_lifecycle: false,
|
||||
error_events: true, reasoning: false, status: false,
|
||||
command_execution: false, file_changes: false, mcp_tools: true,
|
||||
streaming_deltas: false, item_started: false, shared_process: false,
|
||||
},
|
||||
AgentId::Mock => AgentCapabilities {
|
||||
plan_mode: true, permissions: true, questions: true,
|
||||
tool_calls: true, tool_results: true, text_messages: true,
|
||||
images: true, file_attachments: true, session_lifecycle: true,
|
||||
error_events: true, reasoning: true, status: true,
|
||||
command_execution: true, file_changes: true, mcp_tools: true,
|
||||
streaming_deltas: true, item_started: true, shared_process: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Enriched AgentInfo
|
||||
|
||||
Merge v2 install fields with v1 richness:
|
||||
|
||||
```rust
|
||||
pub struct AgentInfo {
|
||||
pub id: String,
|
||||
pub installed: bool, // convenience: is fully installed
|
||||
pub credentials_available: bool, // from credential extraction
|
||||
pub native_required: bool, // keep from v2
|
||||
pub native_installed: bool, // keep from v2
|
||||
pub native_version: Option<String>, // keep from v2
|
||||
pub agent_process_installed: bool, // keep from v2
|
||||
pub agent_process_source: Option<String>, // keep from v2
|
||||
pub agent_process_version: Option<String>, // keep from v2
|
||||
pub path: Option<String>, // from resolve_binary()
|
||||
pub capabilities: AgentCapabilities, // full v1 capability set
|
||||
pub server_status: Option<AgentServerStatus>, // from Feature #6
|
||||
pub models: Option<Vec<AgentModelInfo>>, // optional, installed agents only
|
||||
pub default_model: Option<String>, // optional, installed agents only
|
||||
pub modes: Option<Vec<AgentModeInfo>>, // optional, installed agents only
|
||||
}
|
||||
```
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/router.rs` | Enrich `AgentInfo` and `AgentCapabilities` structs; add `agent_capabilities_for()` static mapping; add credential check; add convenience `installed` field; add optional `models`/`modes` for installed agents |
|
||||
| `server/packages/agent-management/src/agents.rs` | Expose credential availability check and `resolve_binary()` if not already present |
|
||||
| `sdks/typescript/src/client.ts` | Update `AgentInfo` and `AgentCapabilities` types |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Update agent listing test assertions |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/openapi.json` | Update `/v2/agents` response schema with full `AgentCapabilities` |
|
||||
| `docs/sdks/typescript.mdx` | Document enriched agent listing |
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# Feature 13: Models/Modes Listing (Pre-Session)
|
||||
|
||||
**Implementation approach:** Enrich agent response payloads (no separate `/models` or `/modes` endpoints)
|
||||
|
||||
## Summary
|
||||
|
||||
v1 exposed pre-session model/mode discovery via separate endpoints. For v2, models and modes should be optional fields on the agent response payload (only when the agent is installed), with lazy population for dynamic agents.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
- `_sandboxagent/session/list_models` works but requires an active ACP connection and session
|
||||
- `GET /v2/agents` does not include pre-session model/mode metadata
|
||||
- v1 had static per-agent mode definitions (`agent_modes_for()` in `router.rs`)
|
||||
- v1 had dynamic model fetching (Claude/Codex/OpenCode), plus static model lists for Amp/Mock
|
||||
|
||||
## v1 Reference (source commit)
|
||||
|
||||
Use commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836` as the baseline for mode definitions and model-fetching behavior.
|
||||
|
||||
## Response Shape (embedded in agent response)
|
||||
|
||||
Agent payloads should include optional model/mode fields:
|
||||
|
||||
```rust
|
||||
pub struct AgentInfo {
|
||||
// existing fields...
|
||||
pub models: Option<Vec<AgentModelInfo>>, // only present when installed
|
||||
pub default_model: Option<String>, // only present when installed
|
||||
pub modes: Option<Vec<AgentModeInfo>>, // only present when installed
|
||||
}
|
||||
|
||||
pub struct AgentModelInfo {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AgentModeInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
```
|
||||
|
||||
Model variants are explicitly out of scope for this implementation pass.
|
||||
|
||||
## Population Rules
|
||||
|
||||
1. If agent is not installed: omit `models`, `default_model`, and `modes`.
|
||||
2. If installed and static agent (Amp/Mock): populate immediately from static data.
|
||||
3. If installed and dynamic agent (Claude/Codex/OpenCode): lazily start/query backing process and populate response.
|
||||
4. On dynamic-query failure: return the base agent payload and omit model fields, while preserving existing endpoint success semantics.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/router.rs` | Enrich agent response type/handlers to optionally include models + modes |
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Expose model query support for control-plane enrichment without requiring an active session |
|
||||
| `sdks/typescript/src/client.ts` | Extend `AgentInfo` type with optional `models`, `defaultModel`, `modes` |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add assertions for installed vs non-installed agent response shapes |
|
||||
|
||||
## Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/openapi.json` | Update `/v2/agents` (and agent detail endpoint if present) schema with optional `models`/`modes` |
|
||||
| `docs/sdks/typescript.mdx` | Document optional model/mode fields on agent response |
|
||||
132
research/acp/missing-features-spec/14-message-attachments.md
Normal file
132
research/acp/missing-features-spec/14-message-attachments.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Feature 14: Message Attachments
|
||||
|
||||
**Implementation approach:** ACP extension via `_meta` in `session/prompt`
|
||||
|
||||
## Summary
|
||||
|
||||
v1 `MessageRequest.attachments` allowed sending file attachments (path, mime, filename) with prompts. v2 ACP `embeddedContext` is only partial. Need to support file attachments in prompt messages.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
- ACP `session/prompt` accepts `params.content` as the prompt text
|
||||
- No attachment mechanism in the current ACP prompt flow
|
||||
- `embeddedContext` in ACP is for inline context, not file references
|
||||
- The runtime currently passes prompt content through to the agent process as-is
|
||||
|
||||
## v1 Reference (source commit)
|
||||
|
||||
Port behavior from commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`.
|
||||
|
||||
## v1 Types
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct MessageRequest {
|
||||
pub message: String,
|
||||
pub attachments: Option<Vec<MessageAttachment>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct MessageAttachment {
|
||||
pub path: String,
|
||||
pub mime: Option<String>,
|
||||
pub filename: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## v1 Attachment Processing (from `router.rs`)
|
||||
|
||||
```rust
|
||||
fn format_message_with_attachments(message: &str, attachments: &[MessageAttachment]) -> String {
|
||||
if attachments.is_empty() {
|
||||
return message.to_string();
|
||||
}
|
||||
let mut combined = String::new();
|
||||
combined.push_str(message);
|
||||
combined.push_str("\n\nAttachments:\n");
|
||||
for attachment in attachments {
|
||||
combined.push_str("- ");
|
||||
combined.push_str(&attachment.path);
|
||||
combined.push('\n');
|
||||
}
|
||||
combined
|
||||
}
|
||||
|
||||
fn opencode_file_part_input(attachment: &MessageAttachment) -> Value {
|
||||
let path = attachment.path.as_str();
|
||||
let url = if path.starts_with("file://") {
|
||||
path.to_string()
|
||||
} else {
|
||||
format!("file://{path}")
|
||||
};
|
||||
let filename = attachment.filename.clone().or_else(|| {
|
||||
let clean = path.strip_prefix("file://").unwrap_or(path);
|
||||
StdPath::new(clean)
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
});
|
||||
let mut map = serde_json::Map::new();
|
||||
map.insert("type".to_string(), json!("file"));
|
||||
map.insert("mime".to_string(), json!(attachment.mime.clone()
|
||||
.unwrap_or_else(|| "application/octet-stream".to_string())));
|
||||
map.insert("url".to_string(), json!(url));
|
||||
if let Some(filename) = filename {
|
||||
map.insert("filename".to_string(), json!(filename));
|
||||
}
|
||||
Value::Object(map)
|
||||
}
|
||||
```
|
||||
|
||||
### Per-Agent Handling
|
||||
|
||||
- **Claude**: Attachments appended as text to the prompt message (basic)
|
||||
- **OpenCode**: Attachments converted to `file://` URIs in the `input` array using `opencode_file_part_input()`
|
||||
- **Codex**: Attachments converted to file references in the Codex request format
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Extension via `_meta` in `session/prompt`
|
||||
|
||||
Attachments are passed in `_meta.sandboxagent.dev.attachments`:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "session/prompt",
|
||||
"params": {
|
||||
"content": "Review this file",
|
||||
"_meta": {
|
||||
"sandboxagent.dev": {
|
||||
"attachments": [
|
||||
{
|
||||
"path": "/workspace/file.py",
|
||||
"mime": "text/x-python",
|
||||
"filename": "file.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Runtime Processing
|
||||
|
||||
The runtime extracts attachments from `_meta` and transforms them per agent:
|
||||
1. **ACP-native agents**: Forward attachments in `_meta` — the agent process handles them
|
||||
2. **Non-ACP fallback**: Append attachment paths to prompt text (like v1 Claude behavior)
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Extract `attachments` from `session/prompt` `_meta`; transform per agent before forwarding |
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock handling for attachments |
|
||||
| `sdks/typescript/src/client.ts` | Add `attachments` option to prompt method |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add attachment prompt test |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/sdks/typescript.mdx` | Document attachment support in prompts |
|
||||
| `research/acp/spec.md` | Document attachment extension behavior |
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# Feature 15: Session Creation Richness
|
||||
|
||||
**Implementation approach:** Check existing extensions — most already implemented
|
||||
|
||||
## Summary
|
||||
|
||||
v1 `CreateSessionRequest` had `mcp` (full MCP server config with OAuth, env headers, bearer tokens), `skills` (sources with git refs), `agent_version`, `directory`. v2 needs to support these at session creation time.
|
||||
|
||||
## Current v2 State — MOSTLY IMPLEMENTED
|
||||
|
||||
Investigation shows that **most of these are already supported** via `_meta.sandboxagent.dev` passthrough in `session/new`:
|
||||
|
||||
| Field | v1 | v2 Status | v2 Mechanism |
|
||||
|-------|-----|-----------|-------------|
|
||||
| `directory` | `CreateSessionRequest.directory` | **Implemented** | `cwd` parameter extracted from payload |
|
||||
| `agent_version` | `CreateSessionRequest.agent_version` | **Implemented** | `_meta.sandboxagent.dev.agentVersionRequested` (stored, forwarded) |
|
||||
| `skills` | `CreateSessionRequest.skills` | **Implemented** | `_meta.sandboxagent.dev.skills` (stored, forwarded) |
|
||||
| `mcp` | `CreateSessionRequest.mcp` | **Stored but not processed** | `_meta.sandboxagent.dev.mcp` passthrough — stored in `sandbox_meta` but no active MCP server config processing |
|
||||
| `title` | (session metadata) | **Implemented** | `_meta.sandboxagent.dev.title` extracted to `MetaSession.title` |
|
||||
| `requestedSessionId` | (session alias) | **Implemented** | `_meta.sandboxagent.dev.requestedSessionId` |
|
||||
| `model` | `CreateSessionRequest.model` | **Implemented** | `_meta.sandboxagent.dev.model` via `session_model_hint()` |
|
||||
| `variant` | `CreateSessionRequest.variant` | **Deferred** | Out of scope in current implementation pass |
|
||||
|
||||
### Confirmation from `rfds-vs-extensions.md`
|
||||
|
||||
- Skills: "Already extension via `_meta[\"sandboxagent.dev\"].skills` and optional `_sandboxagent/session/set_metadata`"
|
||||
- Agent version: "Already extension via `_meta[\"sandboxagent.dev\"].agentVersionRequested`"
|
||||
- Requested session ID: "Already extension via `_meta[\"sandboxagent.dev\"].requestedSessionId`"
|
||||
|
||||
## v1 Types (for reference)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct CreateSessionRequest {
|
||||
pub agent: String,
|
||||
pub message: String,
|
||||
pub directory: Option<String>,
|
||||
pub variant: Option<String>,
|
||||
pub agent_version: Option<String>,
|
||||
pub mcp: Option<Vec<McpServerConfig>>,
|
||||
pub skills: Option<Vec<SkillSource>>,
|
||||
pub attachments: Option<Vec<MessageAttachment>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ToSchema)]
|
||||
pub struct McpServerConfig {
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
pub args: Option<Vec<String>>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
pub oauth: Option<McpOAuthConfig>,
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
pub bearer_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ToSchema)]
|
||||
pub struct McpOAuthConfig {
|
||||
pub client_id: String,
|
||||
pub client_secret: Option<String>,
|
||||
pub auth_url: String,
|
||||
pub token_url: String,
|
||||
pub scopes: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ToSchema)]
|
||||
pub struct SkillSource {
|
||||
pub name: String,
|
||||
pub source: SkillSourceType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ToSchema)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum SkillSourceType {
|
||||
Git { url: String, ref_spec: Option<String> },
|
||||
Local { path: String },
|
||||
}
|
||||
```
|
||||
|
||||
## What Remains
|
||||
|
||||
### MCP Server Config Processing
|
||||
|
||||
The `mcp` field is stored in `sandbox_meta` but **not actively processed**. To fully support MCP server configuration at session creation:
|
||||
|
||||
1. Extract `_meta.sandboxagent.dev.mcp` array from `session/new` params
|
||||
2. Forward MCP server configs to the agent process (agent-specific: Claude uses `--mcp-config`, Codex/OpenCode have different mechanisms)
|
||||
3. This is complex and agent-specific — may be deferred
|
||||
|
||||
### Recommendation
|
||||
|
||||
Since most fields are already implemented via `_meta` passthrough:
|
||||
- **No new work needed** for `directory`, `agent_version`, `skills`, `title`, `requestedSessionId`, `model`
|
||||
- **MCP config processing** is the only gap — evaluate whether the agent processes already handle MCP config from `_meta` or if explicit processing is needed
|
||||
- Mark this feature as **largely complete** with MCP as a follow-up
|
||||
|
||||
## Files to Modify (if MCP processing is needed)
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Extract and process `mcp` config from `_meta.sandboxagent.dev.mcp` during session creation |
|
||||
| `server/packages/agent-management/src/agents.rs` | Accept MCP config in agent spawn parameters |
|
||||
|
||||
## Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/sdks/typescript.mdx` | Document all supported `_meta.sandboxagent.dev` fields for session creation |
|
||||
170
research/acp/missing-features-spec/16-session-info.md
Normal file
170
research/acp/missing-features-spec/16-session-info.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# Feature 16: Session Info
|
||||
|
||||
**Implementation approach:** New HTTP endpoints (`GET /v2/sessions`, `GET /v2/sessions/{id}`)
|
||||
|
||||
## Summary
|
||||
|
||||
v1 `SessionInfo` tracked `event_count`, `created_at`, `updated_at`, and full `mcp` config. v2 has session data in the ACP runtime's `MetaSession` struct but no HTTP endpoints to query it. Add REST endpoints for session listing and detail.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
### Internal Session Tracking
|
||||
|
||||
From `acp_runtime/mod.rs:130-138`:
|
||||
|
||||
```rust
|
||||
struct MetaSession {
|
||||
session_id: String,
|
||||
agent: AgentId,
|
||||
cwd: String,
|
||||
title: Option<String>,
|
||||
updated_at: Option<String>,
|
||||
sandbox_meta: Map<String, Value>,
|
||||
}
|
||||
```
|
||||
|
||||
### ACP `session/list` Response
|
||||
|
||||
The ACP `session/list` already returns session data (lines 956-967):
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionId": "...",
|
||||
"cwd": "...",
|
||||
"title": "...",
|
||||
"updatedAt": "...",
|
||||
"_meta": { "sandboxagent.dev": { "agent": "claude" } }
|
||||
}
|
||||
```
|
||||
|
||||
But this requires an active ACP connection.
|
||||
|
||||
## v1 Types (exact, from `router.rs`)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SessionInfo {
|
||||
pub session_id: String,
|
||||
pub agent: String,
|
||||
pub agent_mode: String,
|
||||
pub permission_mode: String,
|
||||
pub model: Option<String>,
|
||||
pub native_session_id: Option<String>,
|
||||
pub ended: bool,
|
||||
pub event_count: u64,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
pub directory: Option<String>,
|
||||
pub title: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mcp: Option<BTreeMap<String, McpServerConfig>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub skills: Option<SkillsConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
||||
pub struct SessionListResponse {
|
||||
pub sessions: Vec<SessionInfo>,
|
||||
}
|
||||
```
|
||||
|
||||
## v1 Handler and Builder (exact)
|
||||
|
||||
```rust
|
||||
async fn list_sessions(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<SessionListResponse>, ApiError> {
|
||||
let sessions = state.session_manager.list_sessions().await;
|
||||
Ok(Json(SessionListResponse { sessions }))
|
||||
}
|
||||
|
||||
// SessionManager methods:
|
||||
pub(crate) async fn list_sessions(&self) -> Vec<SessionInfo> {
|
||||
let sessions = self.sessions.lock().await;
|
||||
sessions.iter().rev()
|
||||
.map(|state| Self::build_session_info(state))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_session_info(&self, session_id: &str) -> Option<SessionInfo> {
|
||||
let sessions = self.sessions.lock().await;
|
||||
Self::session_ref(&sessions, session_id).map(Self::build_session_info)
|
||||
}
|
||||
|
||||
fn build_session_info(state: &SessionState) -> SessionInfo {
|
||||
SessionInfo {
|
||||
session_id: state.session_id.clone(),
|
||||
agent: state.agent.as_str().to_string(),
|
||||
agent_mode: state.agent_mode.clone(),
|
||||
permission_mode: state.permission_mode.clone(),
|
||||
model: state.model.clone(),
|
||||
native_session_id: state.native_session_id.clone(),
|
||||
ended: state.ended,
|
||||
event_count: state.events.len() as u64,
|
||||
created_at: state.created_at,
|
||||
updated_at: state.updated_at,
|
||||
directory: state.directory.clone(),
|
||||
title: state.title.clone(),
|
||||
mcp: state.mcp.clone(),
|
||||
skills: state.skills.clone(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### New HTTP Endpoints
|
||||
|
||||
```
|
||||
GET /v2/sessions -> SessionListResponse
|
||||
GET /v2/sessions/{id} -> SessionInfo
|
||||
```
|
||||
|
||||
These are control-plane HTTP endpoints (not ACP), providing session visibility without requiring an active ACP connection.
|
||||
|
||||
### Response Types
|
||||
|
||||
The v2 `SessionInfo` should be a superset of v1 fields, adapted for ACP:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Serialize, JsonSchema, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SessionInfo {
|
||||
pub session_id: String,
|
||||
pub agent: String,
|
||||
pub cwd: String,
|
||||
pub title: Option<String>,
|
||||
pub ended: bool,
|
||||
pub created_at: Option<String>, // ISO 8601 (v1 used i64 timestamp)
|
||||
pub updated_at: Option<String>, // ISO 8601
|
||||
pub model: Option<String>,
|
||||
pub metadata: Value, // full sandbox_meta
|
||||
}
|
||||
```
|
||||
|
||||
### Data Source
|
||||
|
||||
The `AcpRuntime` maintains a `sessions: RwLock<HashMap<String, MetaSession>>` registry. The new HTTP endpoints query this registry.
|
||||
|
||||
Need to add:
|
||||
- `created_at` field to `MetaSession`
|
||||
- `ended` status tracking
|
||||
- Public methods on `AcpRuntime` to expose session list/detail for HTTP handlers
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/router.rs` | Add `GET /v2/sessions` and `GET /v2/sessions/{id}` handlers; add response types |
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add `created_at` to `MetaSession`; add `ended` tracking; expose `list_sessions()` and `get_session()` public methods |
|
||||
| `sdks/typescript/src/client.ts` | Add `listSessions()` and `getSession(id)` methods |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add session listing and detail tests |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/openapi.json` | Add `/v2/sessions` and `/v2/sessions/{id}` endpoint specs |
|
||||
| `docs/cli.mdx` | Add CLI `sessions list` and `sessions info` commands |
|
||||
| `docs/sdks/typescript.mdx` | Document session listing SDK methods |
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
# Feature 17: Error Termination Metadata
|
||||
|
||||
**Implementation approach:** Enrich ACP notifications and session info
|
||||
|
||||
## Summary
|
||||
|
||||
v1 captured `exit_code`, structured `StderrOutput` (head/tail/truncated) when a session ended due to error. v2 loses this metadata. Need to capture and expose process termination details.
|
||||
|
||||
## Current v2 State
|
||||
|
||||
- Agent process lifecycle is managed in `acp_runtime/mod.rs`
|
||||
- Process exit is detected but error metadata (exit code, stderr) is not captured or forwarded
|
||||
- The `_sandboxagent/agent/unparsed` notification exists for parse errors, but not for process crashes
|
||||
- No structured error termination data is emitted to clients
|
||||
|
||||
## v1 Reference (source commit)
|
||||
|
||||
Port behavior and payload shape from commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`.
|
||||
|
||||
## v1 Types (exact, from `universal-agent-schema/src/lib.rs`)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct SessionEndedData {
|
||||
pub reason: SessionEndReason,
|
||||
pub terminated_by: TerminatedBy,
|
||||
/// Error message when reason is Error
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<String>,
|
||||
/// Process exit code when reason is Error
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub exit_code: Option<i32>,
|
||||
/// Agent stderr output when reason is Error
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stderr: Option<StderrOutput>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
pub struct StderrOutput {
|
||||
/// First N lines of stderr (if truncated) or full stderr (if not truncated)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub head: Option<String>,
|
||||
/// Last N lines of stderr (only present if truncated)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tail: Option<String>,
|
||||
/// Whether the output was truncated
|
||||
pub truncated: bool,
|
||||
/// Total number of lines in stderr
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub total_lines: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SessionEndReason {
|
||||
Completed,
|
||||
Error,
|
||||
Terminated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TerminatedBy {
|
||||
Agent,
|
||||
Daemon,
|
||||
}
|
||||
```
|
||||
|
||||
## v1 Implementation (exact)
|
||||
|
||||
### `mark_session_ended` (SessionManager)
|
||||
|
||||
```rust
|
||||
async fn mark_session_ended(
|
||||
&self,
|
||||
session_id: &str,
|
||||
exit_code: Option<i32>,
|
||||
message: &str,
|
||||
reason: SessionEndReason,
|
||||
terminated_by: TerminatedBy,
|
||||
stderr: Option<StderrOutput>,
|
||||
) {
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
if let Some(session) = Self::session_mut(&mut sessions, session_id) {
|
||||
if session.ended { return; }
|
||||
session.mark_ended(exit_code, message.to_string(), reason.clone(), terminated_by.clone());
|
||||
let (error_message, error_exit_code, error_stderr) =
|
||||
if reason == SessionEndReason::Error {
|
||||
(Some(message.to_string()), exit_code, stderr)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
let ended = EventConversion::new(
|
||||
UniversalEventType::SessionEnded,
|
||||
UniversalEventData::SessionEnded(SessionEndedData {
|
||||
reason, terminated_by,
|
||||
message: error_message,
|
||||
exit_code: error_exit_code,
|
||||
stderr: error_stderr,
|
||||
}),
|
||||
).synthetic().with_native_session(session.native_session_id.clone());
|
||||
session.record_conversions(vec![ended]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Stderr capture on error exit
|
||||
|
||||
```rust
|
||||
// Called from consume_spawn when agent process exits with error:
|
||||
Ok(Ok(status)) => {
|
||||
let message = format!("agent exited with status {:?}", status);
|
||||
if !terminate_early {
|
||||
self.record_error(&session_id, message.clone(),
|
||||
Some("process_exit".to_string()), None).await;
|
||||
}
|
||||
let logs = self.read_agent_stderr(agent);
|
||||
self.mark_session_ended(
|
||||
&session_id, status.code(), &message,
|
||||
SessionEndReason::Error, TerminatedBy::Agent, logs,
|
||||
).await;
|
||||
}
|
||||
```
|
||||
|
||||
### Stderr reading
|
||||
|
||||
```rust
|
||||
fn read_agent_stderr(&self, agent: AgentId) -> Option<StderrOutput> {
|
||||
let logs = AgentServerLogs::new(self.server_manager.log_base_dir.clone(), agent.as_str());
|
||||
logs.read_stderr()
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Stderr Capture in ACP Runtime
|
||||
|
||||
When an agent process exits (especially abnormally):
|
||||
|
||||
1. **Capture stderr**: Buffer the agent process's stderr stream with head/tail logic (~50 lines each)
|
||||
2. **Capture exit code**: Get the process exit status
|
||||
3. **Store in session**: Record termination info in the session registry
|
||||
4. **Emit notification**: Send error notification to all connected clients
|
||||
|
||||
### ACP Notification Shape
|
||||
|
||||
When an agent process terminates with an error:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "_sandboxagent/session/ended",
|
||||
"params": {
|
||||
"session_id": "session-uuid",
|
||||
"data": {
|
||||
"reason": "error",
|
||||
"terminated_by": "agent",
|
||||
"message": "agent exited with status ExitStatus(unix_wait_status(256))",
|
||||
"exit_code": 1,
|
||||
"stderr": {
|
||||
"head": "Error: module not found\n at ...",
|
||||
"tail": " at process.exit\nnode exited",
|
||||
"truncated": true,
|
||||
"total_lines": 250
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Session Info Integration
|
||||
|
||||
Termination metadata should be accessible via:
|
||||
- `GET /v2/sessions/{id}` (Feature #16) — include `terminationInfo` in response when session has ended
|
||||
- `session/list` ACP response — include termination status in session entries
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | Add stderr capture (head/tail buffer) on agent process; capture exit code; emit `_sandboxagent/session/ended`; store v1-shaped termination info in `MetaSession` |
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mock.rs` | Add mock error termination scenario (e.g., when prompt contains "crash") |
|
||||
| `sdks/typescript/src/client.ts` | Add `TerminationInfo` type; expose on session events and session info |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | Add error termination metadata test |
|
||||
|
||||
### Docs to Update
|
||||
|
||||
| Doc | Change |
|
||||
|-----|--------|
|
||||
| `docs/sdks/typescript.mdx` | Document `TerminationInfo` type and how to handle error termination |
|
||||
| `research/acp/spec.md` | Document `_sandboxagent/session/ended` extension and payload |
|
||||
30
research/acp/missing-features-spec/feature-index.md
Normal file
30
research/acp/missing-features-spec/feature-index.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Missing Features Index
|
||||
|
||||
Features selected for implementation from the v1-to-v2 gap analysis.
|
||||
|
||||
## Completely UNIMPLEMENTED in v2
|
||||
|
||||
| # | Feature | Implementation notes |
|
||||
|---|---------|---------------------|
|
||||
| 1 | ~~Questions~~ | Deferred to agent process side ([#156](https://github.com/rivet-dev/sandbox-agent/issues/156)) |
|
||||
| 2 | ~~Event history/polling~~ | Not selected |
|
||||
| 3 | ~~Turn stream~~ | Not selected |
|
||||
| 4 | **Filesystem API** -- all 8 endpoints (list, read, write, delete, mkdir, move, stat, upload-batch). ACP only has text-only `fs/read_text_file` + `fs/write_text_file` (agent->client direction). | |
|
||||
| 5 | **Health endpoint** -- typed `HealthResponse` with status. | |
|
||||
| 6 | **Server status** -- `ServerStatus` (Running/Stopped/Error), `ServerStatusInfo` (baseUrl, lastError, restartCount, uptimeMs). | |
|
||||
| 7 | **Session termination** -- v1 had full `terminate`. v2 only has `session/cancel` (turn cancellation, not session kill). No explicit close/delete. | See existing ACP RFD |
|
||||
| 8 | ~~Model variants~~ -- deferred for now. | Out of scope |
|
||||
| 9 | ~~Agent capability flags~~ | Not selected |
|
||||
| 10 | ~~`include_raw`~~ -- deferred for now. | Out of scope |
|
||||
|
||||
## Downgraded / Partial in v2
|
||||
|
||||
| # | Feature | Implementation notes |
|
||||
|---|---------|---------------------|
|
||||
| 11 | ~~Permission reply granularity~~ | Not selected |
|
||||
| 12 | **Agent listing** -- v1 `GET /v1/agents` returned typed `AgentListResponse` with `installed`, `credentialsAvailable`, `path`, `capabilities`, `serverStatus`. v2 returns generic JSON. | |
|
||||
| 13 | **Models/modes listing** -- expose as optional `models`/`modes` fields on agent response payloads (installed agents only), lazily populated. | No separate `/models` or `/modes` endpoints |
|
||||
| 14 | **Message attachments** -- v1 `MessageRequest.attachments` (path, mime, filename). v2 ACP `embeddedContext` is only partial. | |
|
||||
| 15 | **Session creation richness** -- v1 `CreateSessionRequest` had `mcp` (full MCP server config with OAuth, env headers, bearer tokens), `skills` (sources with git refs), `agent_version`, `directory`. Most have no ACP equivalent. | Check with our extensions, do not implement if already done |
|
||||
| 16 | **Session info** -- v1 `SessionInfo` tracked `event_count`, `created_at`, `updated_at`, full `mcp` config. Mostly lost. | Add as sessions HTTP endpoint |
|
||||
| 17 | **Error termination metadata** -- v1 captured `exit_code`, structured `StderrOutput` (head/tail/truncated). Gone. | |
|
||||
132
research/acp/missing-features-spec/plan.md
Normal file
132
research/acp/missing-features-spec/plan.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Missing Features Implementation Plan
|
||||
|
||||
Features selected from the v1-to-v2 gap analysis, ordered for implementation.
|
||||
|
||||
## Confirmed Decisions (Locked)
|
||||
|
||||
- Canonical extension naming is `_sandboxagent/...` and `_meta["sandboxagent.dev"]`; remove/ignore `_sandboxagent/*`.
|
||||
- Control-plane discovery/status/session APIs are HTTP-only under `/v2/*` (no ACP control-plane equivalents).
|
||||
- For Health, Filesystem, and Attachments, implementation should port behavior from v1 using commit `8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836`.
|
||||
- Session termination via `_sandboxagent/session/terminate` is idempotent.
|
||||
- `DELETE /v2/rpc` is transport detach only; it must not replace explicit termination semantics.
|
||||
- Model variants (#8) are removed from current scope.
|
||||
- `include_raw` (#10) is removed from current scope.
|
||||
- Models/modes should be optional properties on agent response payloads (only when the agent is installed) and lazily populated.
|
||||
- Error termination metadata should emit a dedicated session-ended extension event.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
Features are ordered by dependency chain and implementation complexity. Features that other features depend on come first.
|
||||
|
||||
### Phase A: Foundation (control-plane enrichment)
|
||||
|
||||
These features enrich existing endpoints and have no dependencies on each other.
|
||||
|
||||
| Order | Feature | Spec | Approach | Effort |
|
||||
|:-----:|----------------------------------------------|:----:|--------------------------------------------|:------:|
|
||||
| A1 | [Health Endpoint](./05-health-endpoint.md) | #5 | Port v1 health behavior to `GET /v2/health` | Small |
|
||||
| A2 | [Server Status](./06-server-status.md) | #6 | Add process tracking to ACP runtime | Medium |
|
||||
| A3 | [Agent Listing](./12-agent-listing.md) | #12 | Enrich `GET /v2/agents` with v1-parity data | Medium |
|
||||
|
||||
**A2 blocks A3** — agent listing includes server status from Feature #6.
|
||||
|
||||
### Phase B: Session lifecycle
|
||||
|
||||
Session-level features that build on Phase A runtime tracking.
|
||||
|
||||
| Order | Feature | Spec | Approach | Effort |
|
||||
|:-----:|--------------------------------------------------------------|:----:|------------------------------------------------------|:------:|
|
||||
| B1 | [Session Info](./16-session-info.md) | #16 | New `GET /v2/sessions` and `GET /v2/sessions/{id}` | Medium |
|
||||
| B2 | [Session Termination](./07-session-termination.md) | #7 | Idempotent `_sandboxagent/session/terminate` | Medium |
|
||||
| B3 | [Error Termination Metadata](./17-error-termination-metadata.md) | #17 | Stderr capture + `_sandboxagent/session/ended` event | Medium |
|
||||
|
||||
**B2 depends on B1** — terminate updates session state visible via session info.
|
||||
**B3 depends on B1** — termination metadata is stored in session info.
|
||||
|
||||
### Phase C: Agent interaction enrichment
|
||||
|
||||
Features that add richness to the prompt/response cycle.
|
||||
|
||||
| Order | Feature | Spec | Approach | Effort |
|
||||
|:-----:|--------------------------------------------------|:----:|--------------------------------------------------|:------:|
|
||||
| C1 | [Message Attachments](./14-message-attachments.md) | #14 | Port v1 attachment behavior via `session/prompt` | Medium |
|
||||
|
||||
No internal dependencies.
|
||||
|
||||
> **Note:** Questions (#1) deferred to agent process side — see [#156](https://github.com/rivet-dev/sandbox-agent/issues/156).
|
||||
|
||||
### Phase D: Discovery and configuration
|
||||
|
||||
Pre-session discovery and session configuration features.
|
||||
|
||||
| Order | Feature | Spec | Approach | Effort |
|
||||
|:-----:|---------------------------------------------------------|:----:|---------------------------------------------------------|:------:|
|
||||
| D1 | [Models/Modes Listing](./13-models-modes-listing.md) | #13 | Optional `models`/`modes` on agent response, lazy load | Medium |
|
||||
| D2 | [Session Creation Richness](./15-session-creation-richness.md) | #15 | **Mostly done**; MCP config processing remains | Small |
|
||||
|
||||
**D2 is mostly complete** — verify existing `_meta` passthrough; only MCP server config processing may need work.
|
||||
|
||||
### Phase E: Platform services
|
||||
|
||||
Standalone platform-level API.
|
||||
|
||||
| Order | Feature | Spec | Approach | Effort |
|
||||
|:-----:|---------------------------------------|:----:|----------------------------------|:------:|
|
||||
| E1 | [Filesystem API](./04-filesystem-api.md) | #4 | Port v1 behavior to `/v2/fs/*` | Large |
|
||||
|
||||
No dependencies on other features. Can be implemented at any time but is the largest single feature.
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
A1 (Health)
|
||||
A2 (Server Status) ──> A3 (Agent Listing)
|
||||
──> B1 (Session Info) ──> B2 (Session Termination)
|
||||
──> B3 (Error Termination Metadata)
|
||||
|
||||
C1 (Attachments) [independent]
|
||||
|
||||
D1 (Models/Modes on agent response)
|
||||
D2 (Session Creation) [mostly complete]
|
||||
|
||||
E1 (Filesystem) [independent]
|
||||
```
|
||||
|
||||
## Summary Table
|
||||
|
||||
| # | Feature | Spec File | Status | Approach |
|
||||
|:--:|---------------------------------|-------------------------------------------------------|---------------------------------|-------------------------------------------------|
|
||||
| 1 | ~~Questions~~ | [01-questions.md](./01-questions.md) | Deferred ([#156](https://github.com/rivet-dev/sandbox-agent/issues/156)) | Agent process side |
|
||||
| 4 | Filesystem API | [04-filesystem-api.md](./04-filesystem-api.md) | Not implemented | Port v1 behavior onto `/v2/fs/*` |
|
||||
| 5 | Health Endpoint | [05-health-endpoint.md](./05-health-endpoint.md) | Partial (basic only) | Port v1 health behavior |
|
||||
| 6 | Server Status | [06-server-status.md](./06-server-status.md) | Not implemented | Runtime tracking |
|
||||
| 7 | Session Termination | [07-session-termination.md](./07-session-termination.md) | Not implemented | Idempotent ACP extension |
|
||||
| 8 | ~~Model Variants~~ | [08-model-variants.md](./08-model-variants.md) | Deferred (removed from scope) | Do not implement |
|
||||
| 10 | ~~include_raw~~ | [10-include-raw.md](./10-include-raw.md) | Deferred (removed from scope) | Do not implement |
|
||||
| 12 | Agent Listing | [12-agent-listing.md](./12-agent-listing.md) | Partial (install state only) | Enhance existing |
|
||||
| 13 | Models/Modes Listing | [13-models-modes-listing.md](./13-models-modes-listing.md) | Not implemented | Optional agent fields; lazy process start |
|
||||
| 14 | Message Attachments | [14-message-attachments.md](./14-message-attachments.md) | Not implemented | Port v1 behavior via ACP `_meta` |
|
||||
| 15 | Session Creation Richness | [15-session-creation-richness.md](./15-session-creation-richness.md) | **Mostly complete** | Verify existing; MCP config TBD |
|
||||
| 16 | Session Info | [16-session-info.md](./16-session-info.md) | Not implemented | New HTTP endpoints |
|
||||
| 17 | Error Termination Metadata | [17-error-termination-metadata.md](./17-error-termination-metadata.md) | Not implemented | Runtime stderr + `_sandboxagent/session/ended` |
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
### Files modified by multiple features
|
||||
|
||||
| File | Features |
|
||||
|---------------------------------------------------|-------------------------------|
|
||||
| `server/packages/sandbox-agent/src/router.rs` | #4, #5, #6, #12, #13, #16 |
|
||||
| `server/packages/sandbox-agent/src/acp_runtime/mod.rs` | #6, #7, #13, #14, #16, #17 |
|
||||
| `sdks/typescript/src/client.ts` | All in-scope features |
|
||||
| `docs/openapi.json` | #4, #5, #6, #12, #13, #16 |
|
||||
| `docs/sdks/typescript.mdx` | All in-scope features |
|
||||
| `server/packages/sandbox-agent/tests/v2_api.rs` | All in-scope features |
|
||||
|
||||
### Docs update checklist
|
||||
|
||||
- [ ] `docs/openapi.json` — regenerate after all HTTP endpoint changes
|
||||
- [ ] `docs/cli.mdx` — update for new CLI subcommands (#4, #16)
|
||||
- [ ] `docs/sdks/typescript.mdx` — update for all new SDK methods
|
||||
- [ ] `research/acp/spec.md` — update extension methods list
|
||||
- [ ] `research/acp/rfds-vs-extensions.md` — update status of implemented features
|
||||
437
research/acp/old-rest-openapi-list.md
Normal file
437
research/acp/old-rest-openapi-list.md
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
## Sources
|
||||
|
||||
- Old OpenAPI path: `docs/openapi.json` at git ref `8ecd27b`
|
||||
- ACP v2 API path: `~/misc/acp-docs/schema/schema.json` (`~/misc/acp-docs/schema/schema.unstable.json` for unstable methods)
|
||||
|
||||
| Path / Schema Property | ACP Equivalent |
|
||||
| --- | --- |
|
||||
| /v1/agents | UNIMPLEMENTED |
|
||||
| /v1/agents/{agent}/install | UNIMPLEMENTED |
|
||||
| /v1/agents/{agent}/models | session/new.result.models.availableModels (UNSTABLE; session-scoped) |
|
||||
| /v1/agents/{agent}/modes | session/new.result.modes.availableModes (session-scoped) |
|
||||
| /v1/fs/entries | UNIMPLEMENTED |
|
||||
| /v1/fs/entry | UNIMPLEMENTED |
|
||||
| /v1/fs/file | fs/read_text_file + fs/write_text_file (text-only, agent->client direction) |
|
||||
| /v1/fs/mkdir | UNIMPLEMENTED |
|
||||
| /v1/fs/move | UNIMPLEMENTED |
|
||||
| /v1/fs/stat | UNIMPLEMENTED |
|
||||
| /v1/fs/upload-batch | UNIMPLEMENTED |
|
||||
| /v1/health | UNIMPLEMENTED |
|
||||
| /v1/sessions | session/list (UNSTABLE) |
|
||||
| /v1/sessions/{session_id} | session/new \| session/load \| session/resume (UNSTABLE) |
|
||||
| /v1/sessions/{session_id}/events | UNIMPLEMENTED |
|
||||
| /v1/sessions/{session_id}/events/sse | session/update (notification stream) |
|
||||
| /v1/sessions/{session_id}/messages | session/prompt |
|
||||
| /v1/sessions/{session_id}/messages/stream | session/prompt + session/update notifications |
|
||||
| /v1/sessions/{session_id}/permissions/{permission_id}/reply | session/request_permission response |
|
||||
| /v1/sessions/{session_id}/questions/{question_id}/reject | UNIMPLEMENTED |
|
||||
| /v1/sessions/{session_id}/questions/{question_id}/reply | UNIMPLEMENTED |
|
||||
| /v1/sessions/{session_id}/terminate | session/cancel (turn cancellation only) |
|
||||
| AgentCapabilities | initialize.result.agentCapabilities |
|
||||
| AgentCapabilities.commandExecution | UNIMPLEMENTED |
|
||||
| AgentCapabilities.errorEvents | UNIMPLEMENTED |
|
||||
| AgentCapabilities.fileAttachments | initialize.result.agentCapabilities.promptCapabilities.embeddedContext (partial) |
|
||||
| AgentCapabilities.fileChanges | session/update.params.update(sessionUpdate=tool_call_update).content[].type=diff |
|
||||
| AgentCapabilities.images | initialize.result.agentCapabilities.promptCapabilities.image |
|
||||
| AgentCapabilities.itemStarted | session/update.params.update.sessionUpdate=tool_call \| *_message_chunk (no explicit item-start sentinel) |
|
||||
| AgentCapabilities.mcpTools | initialize.result.agentCapabilities.mcpCapabilities |
|
||||
| AgentCapabilities.permissions | session/request_permission (agent->client request) |
|
||||
| AgentCapabilities.planMode | session/update.params.update.sessionUpdate=plan |
|
||||
| AgentCapabilities.questions | UNIMPLEMENTED |
|
||||
| AgentCapabilities.reasoning | session/update.params.update.sessionUpdate=agent_thought_chunk |
|
||||
| AgentCapabilities.sessionLifecycle | session/new, session/load, session/list (UNSTABLE), session/resume (UNSTABLE), session/fork (UNSTABLE) |
|
||||
| AgentCapabilities.sharedProcess | UNIMPLEMENTED |
|
||||
| AgentCapabilities.status | session/update.params.update.tool_call.status \| tool_call_update.status |
|
||||
| AgentCapabilities.streamingDeltas | session/update.params.update.sessionUpdate=agent_message_chunk |
|
||||
| AgentCapabilities.textMessages | ContentBlock.type=text |
|
||||
| AgentCapabilities.toolCalls | session/update.params.update.sessionUpdate=tool_call |
|
||||
| AgentCapabilities.toolResults | session/update.params.update.sessionUpdate=tool_call_update |
|
||||
| AgentError | JSON-RPC Error |
|
||||
| AgentError.agent | Error.data._meta["sandbox-agent/agent"] (extension) |
|
||||
| AgentError.details | Error.data |
|
||||
| AgentError.message | Error.message |
|
||||
| AgentError.session_id | Error.data._meta["sandbox-agent/sessionId"] (extension) |
|
||||
| AgentError.type | Error.code (+ Error.data._meta["sandbox-agent/errorType"] for legacy string types) |
|
||||
| AgentError.type.$ref(ErrorType) | Error.code (+ Error.data._meta["sandbox-agent/errorType"] for legacy string types) |
|
||||
| AgentInfo | initialize.result.agentInfo + initialize.result.agentCapabilities |
|
||||
| AgentInfo.capabilities | initialize.result.agentCapabilities |
|
||||
| AgentInfo.capabilities.$ref(AgentCapabilities) | initialize.result.agentCapabilities |
|
||||
| AgentInfo.credentialsAvailable | UNIMPLEMENTED |
|
||||
| AgentInfo.id | initialize.result.agentInfo.name |
|
||||
| AgentInfo.installed | UNIMPLEMENTED |
|
||||
| AgentInfo.path | UNIMPLEMENTED |
|
||||
| AgentInfo.serverStatus | UNIMPLEMENTED |
|
||||
| AgentInfo.serverStatus.allOf[0] | UNIMPLEMENTED |
|
||||
| AgentInfo.serverStatus.allOf[0].$ref(ServerStatusInfo) | UNIMPLEMENTED |
|
||||
| AgentInfo.version | initialize.result.agentInfo.version |
|
||||
| AgentInstallRequest | UNIMPLEMENTED |
|
||||
| AgentInstallRequest.reinstall | UNIMPLEMENTED |
|
||||
| AgentListResponse | UNIMPLEMENTED |
|
||||
| AgentListResponse.agents | UNIMPLEMENTED |
|
||||
| AgentListResponse.agents[] | UNIMPLEMENTED |
|
||||
| AgentListResponse.agents[].$ref(AgentInfo) | UNIMPLEMENTED |
|
||||
| AgentModeInfo | SessionMode |
|
||||
| AgentModeInfo.description | SessionMode.description |
|
||||
| AgentModeInfo.id | SessionMode.id |
|
||||
| AgentModeInfo.name | SessionMode.name |
|
||||
| AgentModelInfo | ModelInfo (UNSTABLE) |
|
||||
| AgentModelInfo.defaultVariant | UNIMPLEMENTED |
|
||||
| AgentModelInfo.id | ModelInfo.modelId (UNSTABLE) |
|
||||
| AgentModelInfo.name | ModelInfo.name (UNSTABLE) |
|
||||
| AgentModelInfo.variants | UNIMPLEMENTED |
|
||||
| AgentModelInfo.variants[] | UNIMPLEMENTED |
|
||||
| AgentModelsResponse | SessionModelState (UNSTABLE) |
|
||||
| AgentModelsResponse.defaultModel | SessionModelState.currentModelId (UNSTABLE) |
|
||||
| AgentModelsResponse.models | SessionModelState.availableModels (UNSTABLE) |
|
||||
| AgentModelsResponse.models[] | SessionModelState.availableModels[] (UNSTABLE) |
|
||||
| AgentModelsResponse.models[].$ref(AgentModelInfo) | SessionModelState.availableModels[] -> ModelInfo (UNSTABLE) |
|
||||
| AgentModesResponse | SessionModeState |
|
||||
| AgentModesResponse.modes | SessionModeState.availableModes |
|
||||
| AgentModesResponse.modes[] | SessionModeState.availableModes[] |
|
||||
| AgentModesResponse.modes[].$ref(AgentModeInfo) | SessionModeState.availableModes[] -> SessionMode |
|
||||
| AgentUnparsedData | session/update.params.update._meta["sandbox-agent/unparsed"] (extension) |
|
||||
| AgentUnparsedData.error | session/update.params.update._meta["sandbox-agent/unparsed"].error (extension) |
|
||||
| AgentUnparsedData.location | session/update.params.update._meta["sandbox-agent/unparsed"].location (extension) |
|
||||
| AgentUnparsedData.raw_hash | session/update.params.update._meta["sandbox-agent/unparsed"].rawHash (extension) |
|
||||
| ContentPart | ContentBlock \| ToolCall \| ToolCallUpdate \| ToolCallContent |
|
||||
| ContentPart.oneOf[0] | ContentBlock (type=text) |
|
||||
| ContentPart.oneOf[0].text | ContentBlock.text |
|
||||
| ContentPart.oneOf[0].type | ContentBlock.type="text" |
|
||||
| ContentPart.oneOf[1] | ContentBlock (type=resource) with JSON payload |
|
||||
| ContentPart.oneOf[1].json | EmbeddedResource.resource.text (mimeType="application/json") |
|
||||
| ContentPart.oneOf[1].type | ContentBlock.type="resource" |
|
||||
| ContentPart.oneOf[2] | session/update.params.update(sessionUpdate=tool_call) |
|
||||
| ContentPart.oneOf[2].arguments | ToolCall.rawInput |
|
||||
| ContentPart.oneOf[2].call_id | ToolCall.toolCallId |
|
||||
| ContentPart.oneOf[2].name | ToolCall._meta["sandbox-agent/toolName"] (extension) |
|
||||
| ContentPart.oneOf[2].type | session/update.params.update.sessionUpdate="tool_call" |
|
||||
| ContentPart.oneOf[3] | session/update.params.update(sessionUpdate=tool_call_update) |
|
||||
| ContentPart.oneOf[3].call_id | ToolCallUpdate.toolCallId |
|
||||
| ContentPart.oneOf[3].output | ToolCallUpdate.rawOutput |
|
||||
| ContentPart.oneOf[3].type | session/update.params.update.sessionUpdate="tool_call_update" |
|
||||
| ContentPart.oneOf[4] | ToolCallContent (type=diff) |
|
||||
| ContentPart.oneOf[4].action | ToolCall.kind (read->read, write/patch->edit) |
|
||||
| ContentPart.oneOf[4].action.$ref(FileAction) | ToolKind (read\|edit) |
|
||||
| ContentPart.oneOf[4].diff | Diff.newText / Diff.oldText |
|
||||
| ContentPart.oneOf[4].path | Diff.path |
|
||||
| ContentPart.oneOf[4].type | ToolCallContent.type="diff" |
|
||||
| ContentPart.oneOf[5] | session/update.params.update(sessionUpdate=agent_thought_chunk) |
|
||||
| ContentPart.oneOf[5].text | ContentChunk.content.text |
|
||||
| ContentPart.oneOf[5].type | session/update.params.update.sessionUpdate="agent_thought_chunk" |
|
||||
| ContentPart.oneOf[5].visibility | ContentChunk.content._meta["sandbox-agent/reasoningVisibility"] (extension) |
|
||||
| ContentPart.oneOf[5].visibility.$ref(ReasoningVisibility) | ContentChunk.content._meta["sandbox-agent/reasoningVisibility"] (extension) |
|
||||
| ContentPart.oneOf[6] | ContentBlock (type=image) |
|
||||
| ContentPart.oneOf[6].mime | ImageContent.mimeType |
|
||||
| ContentPart.oneOf[6].path | ImageContent.uri |
|
||||
| ContentPart.oneOf[6].type | ContentBlock.type="image" |
|
||||
| ContentPart.oneOf[7] | session/update.params.update._meta["sandbox-agent/status"] (extension) |
|
||||
| ContentPart.oneOf[7].detail | session/update.params.update._meta["sandbox-agent/status"].detail (extension) |
|
||||
| ContentPart.oneOf[7].label | session/update.params.update._meta["sandbox-agent/status"].label (extension) |
|
||||
| ContentPart.oneOf[7].type | session/update.params.update._meta["sandbox-agent/status"].type (extension) |
|
||||
| CreateSessionRequest | session/new.params (+ session/set_mode, session/set_model UNSTABLE, session/set_config_option, _meta extensions) |
|
||||
| CreateSessionRequest.agent | session/new.params._meta["sandbox-agent/agent"] (extension; agent selection is out-of-band in ACP) |
|
||||
| CreateSessionRequest.agentMode | session/set_mode.params.modeId (or session/new.params._meta["sandbox-agent/agentMode"]) |
|
||||
| CreateSessionRequest.agentVersion | session/new.params._meta["sandbox-agent/agentVersion"] (extension) |
|
||||
| CreateSessionRequest.directory | session/new.params.cwd |
|
||||
| CreateSessionRequest.mcp | session/new.params.mcpServers |
|
||||
| CreateSessionRequest.mcp.* | session/new.params.mcpServers[] |
|
||||
| CreateSessionRequest.mcp.*.$ref(McpServerConfig) | McpServer |
|
||||
| CreateSessionRequest.model | session/set_model.params.modelId (UNSTABLE) or session/set_config_option (category=model) |
|
||||
| CreateSessionRequest.permissionMode | session/set_config_option.params (extension-defined option) or _meta |
|
||||
| CreateSessionRequest.skills | session/new.params._meta["sandbox-agent/skills"] (extension) |
|
||||
| CreateSessionRequest.skills.allOf[0] | session/new.params._meta["sandbox-agent/skills"] (extension) |
|
||||
| CreateSessionRequest.skills.allOf[0].$ref(SkillsConfig) | session/new.params._meta["sandbox-agent/skills"] (extension) |
|
||||
| CreateSessionRequest.title | session/new.params._meta["sandbox-agent/title"] (extension); session/update(session_info_update).title |
|
||||
| CreateSessionRequest.variant | session/new.params._meta["sandbox-agent/variant"] (extension) |
|
||||
| CreateSessionResponse | session/new.result |
|
||||
| CreateSessionResponse.error | JSON-RPC error |
|
||||
| CreateSessionResponse.error.allOf[0] | JSON-RPC error |
|
||||
| CreateSessionResponse.error.allOf[0].$ref(AgentError) | JSON-RPC error |
|
||||
| CreateSessionResponse.healthy | JSON-RPC success (no error) |
|
||||
| CreateSessionResponse.nativeSessionId | session/new.result.sessionId |
|
||||
| ErrorData | JSON-RPC Error |
|
||||
| ErrorData.code | Error.code |
|
||||
| ErrorData.details | Error.data |
|
||||
| ErrorData.message | Error.message |
|
||||
| ErrorType | Error.code (+ Error.data._meta["sandbox-agent/errorType"] for legacy string values) |
|
||||
| EventSource | session/update.params._meta["sandbox-agent/source"] (extension) |
|
||||
| EventsQuery | UNIMPLEMENTED |
|
||||
| EventsQuery.includeRaw | UNIMPLEMENTED |
|
||||
| EventsQuery.limit | UNIMPLEMENTED |
|
||||
| EventsQuery.offset | UNIMPLEMENTED |
|
||||
| EventsResponse | Stream of session/update notifications |
|
||||
| EventsResponse.events | session/update notifications |
|
||||
| EventsResponse.events[] | SessionNotification |
|
||||
| EventsResponse.events[].$ref(UniversalEvent) | SessionNotification (+ JSON-RPC envelope) |
|
||||
| EventsResponse.hasMore | UNIMPLEMENTED |
|
||||
| FileAction | ToolKind (read->read, write/patch->edit) |
|
||||
| FsActionResponse | UNIMPLEMENTED |
|
||||
| FsActionResponse.path | UNIMPLEMENTED |
|
||||
| FsDeleteQuery | UNIMPLEMENTED |
|
||||
| FsDeleteQuery.path | UNIMPLEMENTED |
|
||||
| FsDeleteQuery.recursive | UNIMPLEMENTED |
|
||||
| FsDeleteQuery.sessionId | UNIMPLEMENTED |
|
||||
| FsEntriesQuery | UNIMPLEMENTED |
|
||||
| FsEntriesQuery.path | UNIMPLEMENTED |
|
||||
| FsEntriesQuery.sessionId | UNIMPLEMENTED |
|
||||
| FsEntry | UNIMPLEMENTED |
|
||||
| FsEntry.entryType | UNIMPLEMENTED |
|
||||
| FsEntry.entryType.$ref(FsEntryType) | UNIMPLEMENTED |
|
||||
| FsEntry.modified | UNIMPLEMENTED |
|
||||
| FsEntry.name | UNIMPLEMENTED |
|
||||
| FsEntry.path | UNIMPLEMENTED |
|
||||
| FsEntry.size | UNIMPLEMENTED |
|
||||
| FsEntryType | UNIMPLEMENTED |
|
||||
| FsMoveRequest | UNIMPLEMENTED |
|
||||
| FsMoveRequest.from | UNIMPLEMENTED |
|
||||
| FsMoveRequest.overwrite | UNIMPLEMENTED |
|
||||
| FsMoveRequest.to | UNIMPLEMENTED |
|
||||
| FsMoveResponse | UNIMPLEMENTED |
|
||||
| FsMoveResponse.from | UNIMPLEMENTED |
|
||||
| FsMoveResponse.to | UNIMPLEMENTED |
|
||||
| FsPathQuery | fs/read_text_file.params \| fs/write_text_file.params (partial) |
|
||||
| FsPathQuery.path | fs/read_text_file.params.path \| fs/write_text_file.params.path |
|
||||
| FsPathQuery.sessionId | fs/read_text_file.params.sessionId \| fs/write_text_file.params.sessionId |
|
||||
| FsSessionQuery | fs/read_text_file.params \| fs/write_text_file.params (partial) |
|
||||
| FsSessionQuery.sessionId | fs/read_text_file.params.sessionId \| fs/write_text_file.params.sessionId |
|
||||
| FsStat | UNIMPLEMENTED |
|
||||
| FsStat.entryType | UNIMPLEMENTED |
|
||||
| FsStat.entryType.$ref(FsEntryType) | UNIMPLEMENTED |
|
||||
| FsStat.modified | UNIMPLEMENTED |
|
||||
| FsStat.path | UNIMPLEMENTED |
|
||||
| FsStat.size | UNIMPLEMENTED |
|
||||
| FsUploadBatchQuery | UNIMPLEMENTED |
|
||||
| FsUploadBatchQuery.path | UNIMPLEMENTED |
|
||||
| FsUploadBatchQuery.sessionId | UNIMPLEMENTED |
|
||||
| FsUploadBatchResponse | UNIMPLEMENTED |
|
||||
| FsUploadBatchResponse.paths | UNIMPLEMENTED |
|
||||
| FsUploadBatchResponse.paths[] | UNIMPLEMENTED |
|
||||
| FsUploadBatchResponse.truncated | UNIMPLEMENTED |
|
||||
| FsWriteResponse | fs/write_text_file.result (partial) |
|
||||
| FsWriteResponse.bytesWritten | UNIMPLEMENTED |
|
||||
| FsWriteResponse.path | fs/write_text_file.params.path |
|
||||
| HealthResponse | UNIMPLEMENTED |
|
||||
| HealthResponse.status | UNIMPLEMENTED |
|
||||
| ItemDeltaData | session/update.params.update(sessionUpdate=*message_chunk) |
|
||||
| ItemDeltaData.delta | ContentChunk.content.text |
|
||||
| ItemDeltaData.item_id | ContentChunk._meta["sandbox-agent/itemId"] (extension) |
|
||||
| ItemDeltaData.native_item_id | ContentChunk._meta["sandbox-agent/nativeItemId"] (extension) |
|
||||
| ItemEventData | session/update.params.update |
|
||||
| ItemEventData.item | SessionUpdate (ToolCall \| ToolCallUpdate \| ContentChunk) |
|
||||
| ItemEventData.item.$ref(UniversalItem) | SessionUpdate (ToolCall \| ToolCallUpdate \| ContentChunk) |
|
||||
| ItemKind | SessionUpdate.sessionUpdate + ToolKind |
|
||||
| ItemRole | Role (assistant\|user) + SessionUpdate.sessionUpdate for non-message items |
|
||||
| ItemStatus | ToolCall.status \| ToolCallUpdate.status |
|
||||
| McpCommand | McpServerStdio.command + McpServerStdio.args |
|
||||
| McpCommand.oneOf[0] | McpServerStdio.command |
|
||||
| McpCommand.oneOf[1] | McpServerStdio.args |
|
||||
| McpCommand.oneOf[1][] | McpServerStdio.args[] |
|
||||
| McpOAuthConfig | McpServer._meta["sandbox-agent/oauth"] (extension) |
|
||||
| McpOAuthConfig.clientId | McpServer._meta["sandbox-agent/oauth"].clientId (extension) |
|
||||
| McpOAuthConfig.clientSecret | McpServer._meta["sandbox-agent/oauth"].clientSecret (extension) |
|
||||
| McpOAuthConfig.scope | McpServer._meta["sandbox-agent/oauth"].scope (extension) |
|
||||
| McpOAuthConfigOrDisabled | McpServer._meta["sandbox-agent/oauth"] (extension) |
|
||||
| McpOAuthConfigOrDisabled.oneOf[0] | McpServer._meta["sandbox-agent/oauth"] (extension) |
|
||||
| McpOAuthConfigOrDisabled.oneOf[0].$ref(McpOAuthConfig) | McpServer._meta["sandbox-agent/oauth"] (extension) |
|
||||
| McpOAuthConfigOrDisabled.oneOf[1] | McpServer._meta["sandbox-agent/oauthDisabled"] (extension) |
|
||||
| McpRemoteTransport | McpServer.type ("http" \| "sse") |
|
||||
| McpServerConfig | McpServer |
|
||||
| McpServerConfig.oneOf[0] | McpServerStdio (McpServer.type="stdio") |
|
||||
| McpServerConfig.oneOf[0].args | McpServerStdio.args |
|
||||
| McpServerConfig.oneOf[0].args[] | McpServerStdio.args[] |
|
||||
| McpServerConfig.oneOf[0].command | McpServerStdio.command |
|
||||
| McpServerConfig.oneOf[0].command.$ref(McpCommand) | McpServerStdio.command + McpServerStdio.args |
|
||||
| McpServerConfig.oneOf[0].cwd | McpServerStdio._meta["sandbox-agent/cwd"] (extension) |
|
||||
| McpServerConfig.oneOf[0].enabled | McpServerStdio._meta["sandbox-agent/enabled"] (extension) |
|
||||
| McpServerConfig.oneOf[0].env | McpServerStdio.env (object -> EnvVariable[]) |
|
||||
| McpServerConfig.oneOf[0].env.* | McpServerStdio.env[].{name,value} |
|
||||
| McpServerConfig.oneOf[0].timeoutMs | McpServerStdio._meta["sandbox-agent/timeoutMs"] (extension) |
|
||||
| McpServerConfig.oneOf[0].type | McpServer type="stdio" |
|
||||
| McpServerConfig.oneOf[1] | McpServerHttp \| McpServerSse |
|
||||
| McpServerConfig.oneOf[1].bearerTokenEnvVar | McpServer._meta["sandbox-agent/bearerTokenEnvVar"] (extension) |
|
||||
| McpServerConfig.oneOf[1].enabled | McpServer._meta["sandbox-agent/enabled"] (extension) |
|
||||
| McpServerConfig.oneOf[1].envHeaders | McpServer._meta["sandbox-agent/envHeaders"] (extension) |
|
||||
| McpServerConfig.oneOf[1].envHeaders.* | McpServer._meta["sandbox-agent/envHeaders"] (extension) |
|
||||
| McpServerConfig.oneOf[1].headers | McpServerHttp.headers \| McpServerSse.headers (object -> HttpHeader[]) |
|
||||
| McpServerConfig.oneOf[1].headers.* | McpServerHttp.headers[].{name,value} \| McpServerSse.headers[].{name,value} |
|
||||
| McpServerConfig.oneOf[1].oauth | McpServer._meta["sandbox-agent/oauth"] (extension) |
|
||||
| McpServerConfig.oneOf[1].oauth.allOf[0] | McpServer._meta["sandbox-agent/oauth"] (extension) |
|
||||
| McpServerConfig.oneOf[1].oauth.allOf[0].$ref(McpOAuthConfigOrDisabled) | McpServer._meta["sandbox-agent/oauth"] (extension) |
|
||||
| McpServerConfig.oneOf[1].timeoutMs | McpServer._meta["sandbox-agent/timeoutMs"] (extension) |
|
||||
| McpServerConfig.oneOf[1].transport | McpServer.type ("http" \| "sse") |
|
||||
| McpServerConfig.oneOf[1].transport.allOf[0] | McpServer.type ("http" \| "sse") |
|
||||
| McpServerConfig.oneOf[1].transport.allOf[0].$ref(McpRemoteTransport) | McpServer.type ("http" \| "sse") |
|
||||
| McpServerConfig.oneOf[1].type | McpServer.type ("http" \| "sse") |
|
||||
| McpServerConfig.oneOf[1].url | McpServerHttp.url \| McpServerSse.url |
|
||||
| MessageAttachment | ContentBlock.resource_link \| ContentBlock.image |
|
||||
| MessageAttachment.filename | ResourceLink.name \| ResourceLink.title |
|
||||
| MessageAttachment.mime | ResourceLink.mimeType \| ImageContent.mimeType |
|
||||
| MessageAttachment.path | ResourceLink.uri \| ImageContent.uri |
|
||||
| MessageRequest | session/prompt.params |
|
||||
| MessageRequest.attachments | session/prompt.params.prompt[] (non-text ContentBlock entries) |
|
||||
| MessageRequest.attachments[] | session/prompt.params.prompt[] (resource_link/image/resource) |
|
||||
| MessageRequest.attachments[].$ref(MessageAttachment) | ContentBlock.resource_link \| ContentBlock.image |
|
||||
| MessageRequest.message | session/prompt.params.prompt[]: ContentBlock(type=text).text |
|
||||
| PermissionEventData | session/request_permission.params + response |
|
||||
| PermissionEventData.action | session/request_permission.params.toolCall.title \| .kind |
|
||||
| PermissionEventData.metadata | session/request_permission.params._meta (extension) |
|
||||
| PermissionEventData.permission_id | JSON-RPC request id of session/request_permission |
|
||||
| PermissionEventData.status | session/request_permission lifecycle + RequestPermissionOutcome |
|
||||
| PermissionEventData.status.$ref(PermissionStatus) | RequestPermissionOutcome + PermissionOption.kind mapping |
|
||||
| PermissionReply | RequestPermissionOutcome.selected.optionId (mapped from chosen PermissionOption) |
|
||||
| PermissionReplyRequest | session/request_permission response |
|
||||
| PermissionReplyRequest.reply | RequestPermissionResponse.outcome |
|
||||
| PermissionReplyRequest.reply.$ref(PermissionReply) | RequestPermissionResponse.outcome |
|
||||
| PermissionStatus | RequestPermissionOutcome + PermissionOption.kind (allow_once\|allow_always\|reject_once\|reject_always) |
|
||||
| ProblemDetails | JSON-RPC Error (partial) |
|
||||
| ProblemDetails.detail | Error.data.detail (or Error.message) |
|
||||
| ProblemDetails.instance | Error.data.instance |
|
||||
| ProblemDetails.status | Error.code |
|
||||
| ProblemDetails.title | Error.message |
|
||||
| ProblemDetails.type | Error.data.type |
|
||||
| ProblemDetails.* | Error.data.* |
|
||||
| QuestionEventData | UNIMPLEMENTED |
|
||||
| QuestionEventData.options | UNIMPLEMENTED |
|
||||
| QuestionEventData.options[] | UNIMPLEMENTED |
|
||||
| QuestionEventData.prompt | UNIMPLEMENTED |
|
||||
| QuestionEventData.question_id | UNIMPLEMENTED |
|
||||
| QuestionEventData.response | UNIMPLEMENTED |
|
||||
| QuestionEventData.status | UNIMPLEMENTED |
|
||||
| QuestionEventData.status.$ref(QuestionStatus) | UNIMPLEMENTED |
|
||||
| QuestionReplyRequest | UNIMPLEMENTED |
|
||||
| QuestionReplyRequest.answers | UNIMPLEMENTED |
|
||||
| QuestionReplyRequest.answers[] | UNIMPLEMENTED |
|
||||
| QuestionReplyRequest.answers[][] | UNIMPLEMENTED |
|
||||
| QuestionStatus | UNIMPLEMENTED |
|
||||
| ReasoningVisibility | Content._meta["sandbox-agent/reasoningVisibility"] (extension) |
|
||||
| ServerStatus | UNIMPLEMENTED |
|
||||
| ServerStatusInfo | UNIMPLEMENTED |
|
||||
| ServerStatusInfo.baseUrl | UNIMPLEMENTED |
|
||||
| ServerStatusInfo.lastError | UNIMPLEMENTED |
|
||||
| ServerStatusInfo.restartCount | UNIMPLEMENTED |
|
||||
| ServerStatusInfo.status | UNIMPLEMENTED |
|
||||
| ServerStatusInfo.status.$ref(ServerStatus) | UNIMPLEMENTED |
|
||||
| ServerStatusInfo.uptimeMs | UNIMPLEMENTED |
|
||||
| SessionEndedData | session/prompt.result.stopReason (+ _meta extension for process details) |
|
||||
| SessionEndedData.exit_code | session/prompt.result._meta["sandbox-agent/sessionEnd"].exitCode (extension) |
|
||||
| SessionEndedData.message | session/prompt.result._meta["sandbox-agent/sessionEnd"].message (extension) |
|
||||
| SessionEndedData.reason | session/prompt.result.stopReason |
|
||||
| SessionEndedData.reason.$ref(SessionEndReason) | session/prompt.result.stopReason |
|
||||
| SessionEndedData.stderr | session/prompt.result._meta["sandbox-agent/sessionEnd"].stderr (extension) |
|
||||
| SessionEndedData.stderr.allOf[0] | session/prompt.result._meta["sandbox-agent/sessionEnd"].stderr (extension) |
|
||||
| SessionEndedData.stderr.allOf[0].$ref(StderrOutput) | session/prompt.result._meta["sandbox-agent/sessionEnd"].stderr (extension) |
|
||||
| SessionEndedData.terminated_by | session/prompt.result._meta["sandbox-agent/sessionEnd"].terminatedBy (extension) |
|
||||
| SessionEndedData.terminated_by.$ref(TerminatedBy) | session/prompt.result._meta["sandbox-agent/sessionEnd"].terminatedBy (extension) |
|
||||
| SessionEndReason | StopReason (completed->end_turn, terminated->cancelled, error->JSON-RPC error or refusal) |
|
||||
| SessionInfo | session/list.result.sessions[] (UNSTABLE) + session/update(session_info_update) |
|
||||
| SessionInfo.agent | SessionInfo._meta["sandbox-agent/agent"] (extension) |
|
||||
| SessionInfo.agentMode | SessionModeState.currentModeId / CurrentModeUpdate.currentModeId |
|
||||
| SessionInfo.createdAt | SessionInfo._meta["sandbox-agent/createdAt"] (extension) |
|
||||
| SessionInfo.directory | SessionInfo.cwd |
|
||||
| SessionInfo.ended | SessionInfo._meta["sandbox-agent/ended"] (extension) |
|
||||
| SessionInfo.eventCount | SessionInfo._meta["sandbox-agent/eventCount"] (extension) |
|
||||
| SessionInfo.mcp | SessionInfo._meta["sandbox-agent/mcp"] (extension) |
|
||||
| SessionInfo.mcp.* | SessionInfo._meta["sandbox-agent/mcp"][*] (extension) |
|
||||
| SessionInfo.mcp.*.$ref(McpServerConfig) | SessionInfo._meta["sandbox-agent/mcp"][*] (extension) |
|
||||
| SessionInfo.model | SessionModelState.currentModelId (UNSTABLE) or SessionInfo._meta["sandbox-agent/model"] |
|
||||
| SessionInfo.nativeSessionId | SessionInfo._meta["sandbox-agent/nativeSessionId"] (extension) |
|
||||
| SessionInfo.permissionMode | SessionInfo._meta["sandbox-agent/permissionMode"] (extension) |
|
||||
| SessionInfo.sessionId | SessionInfo.sessionId |
|
||||
| SessionInfo.skills | SessionInfo._meta["sandbox-agent/skills"] (extension) |
|
||||
| SessionInfo.skills.allOf[0] | SessionInfo._meta["sandbox-agent/skills"] (extension) |
|
||||
| SessionInfo.skills.allOf[0].$ref(SkillsConfig) | SessionInfo._meta["sandbox-agent/skills"] (extension) |
|
||||
| SessionInfo.title | SessionInfo.title \| SessionInfoUpdate.title |
|
||||
| SessionInfo.updatedAt | SessionInfo.updatedAt \| SessionInfoUpdate.updatedAt |
|
||||
| SessionInfo.variant | SessionInfo._meta["sandbox-agent/variant"] (extension) |
|
||||
| SessionListResponse | session/list.result (UNSTABLE) |
|
||||
| SessionListResponse.sessions | session/list.result.sessions (UNSTABLE) |
|
||||
| SessionListResponse.sessions[] | session/list.result.sessions[] (UNSTABLE) |
|
||||
| SessionListResponse.sessions[].$ref(SessionInfo) | session/list.result.sessions[] -> SessionInfo (UNSTABLE) |
|
||||
| SessionStartedData | session/new.result (+ _meta extensions) |
|
||||
| SessionStartedData.metadata | session/new.result._meta (extension) |
|
||||
| SkillsConfig | session/new.params._meta["sandbox-agent/skills"] (extension) |
|
||||
| SkillsConfig.sources | session/new.params._meta["sandbox-agent/skills"].sources (extension) |
|
||||
| SkillsConfig.sources[] | session/new.params._meta["sandbox-agent/skills"].sources[] (extension) |
|
||||
| SkillsConfig.sources[].$ref(SkillSource) | session/new.params._meta["sandbox-agent/skills"].sources[] (extension) |
|
||||
| SkillSource | session/new.params._meta["sandbox-agent/skills"].sources[] (extension) |
|
||||
| SkillSource.ref | session/new.params._meta["sandbox-agent/skills"].sources[].ref (extension) |
|
||||
| SkillSource.skills | session/new.params._meta["sandbox-agent/skills"].sources[].skills (extension) |
|
||||
| SkillSource.skills[] | session/new.params._meta["sandbox-agent/skills"].sources[].skills[] (extension) |
|
||||
| SkillSource.source | session/new.params._meta["sandbox-agent/skills"].sources[].source (extension) |
|
||||
| SkillSource.subpath | session/new.params._meta["sandbox-agent/skills"].sources[].subpath (extension) |
|
||||
| SkillSource.type | session/new.params._meta["sandbox-agent/skills"].sources[].type (extension) |
|
||||
| StderrOutput | session/prompt.result._meta["sandbox-agent/sessionEnd"].stderr (extension) |
|
||||
| StderrOutput.head | session/prompt.result._meta["sandbox-agent/sessionEnd"].stderr.head (extension) |
|
||||
| StderrOutput.tail | session/prompt.result._meta["sandbox-agent/sessionEnd"].stderr.tail (extension) |
|
||||
| StderrOutput.total_lines | session/prompt.result._meta["sandbox-agent/sessionEnd"].stderr.totalLines (extension) |
|
||||
| StderrOutput.truncated | session/prompt.result._meta["sandbox-agent/sessionEnd"].stderr.truncated (extension) |
|
||||
| TerminatedBy | session/prompt.result._meta["sandbox-agent/sessionEnd"].terminatedBy (extension) |
|
||||
| TurnEventData | session/prompt lifecycle (request start -> response end) |
|
||||
| TurnEventData.metadata | session/prompt.params._meta \| session/prompt.result._meta |
|
||||
| TurnEventData.phase | session/prompt lifecycle phase |
|
||||
| TurnEventData.phase.$ref(TurnPhase) | session/prompt lifecycle phase |
|
||||
| TurnEventData.turn_id | session/prompt.params._meta["sandbox-agent/turnId"] (extension) |
|
||||
| TurnPhase | session/prompt lifecycle: started=request received, ended=prompt response returned |
|
||||
| TurnStreamQuery | UNIMPLEMENTED |
|
||||
| TurnStreamQuery.includeRaw | UNIMPLEMENTED |
|
||||
| UniversalEvent | JSON-RPC envelope + SessionNotification(session/update) + request/response events |
|
||||
| UniversalEvent.data | session/update.params.update |
|
||||
| UniversalEvent.data.$ref(UniversalEventData) | SessionUpdate \| JSON-RPC Error \| session/request_permission |
|
||||
| UniversalEvent.event_id | session/update.params._meta["sandbox-agent/eventId"] (extension) |
|
||||
| UniversalEvent.native_session_id | session/update.params._meta["sandbox-agent/nativeSessionId"] (extension) |
|
||||
| UniversalEvent.raw | session/update.params._meta["sandbox-agent/raw"] (extension) |
|
||||
| UniversalEvent.sequence | session/update.params._meta["sandbox-agent/sequence"] (extension) |
|
||||
| UniversalEvent.session_id | session/update.params.sessionId |
|
||||
| UniversalEvent.source | session/update.params._meta["sandbox-agent/source"] (extension) |
|
||||
| UniversalEvent.source.$ref(EventSource) | session/update.params._meta["sandbox-agent/source"] (extension) |
|
||||
| UniversalEvent.synthetic | session/update.params._meta["sandbox-agent/synthetic"] (extension) |
|
||||
| UniversalEvent.time | session/update.params._meta["sandbox-agent/time"] (extension) |
|
||||
| UniversalEvent.type | session/update.params.update.sessionUpdate \| JSON-RPC method type |
|
||||
| UniversalEvent.type.$ref(UniversalEventType) | session/update.params.update.sessionUpdate \| JSON-RPC method type |
|
||||
| UniversalEventData | SessionUpdate \| JSON-RPC Error \| session/request_permission |
|
||||
| UniversalEventData.oneOf[0] | session/prompt lifecycle (turn metadata in _meta) |
|
||||
| UniversalEventData.oneOf[0].$ref(TurnEventData) | session/prompt lifecycle (request/response boundary) |
|
||||
| UniversalEventData.oneOf[1] | session/new.result |
|
||||
| UniversalEventData.oneOf[1].$ref(SessionStartedData) | session/new.result |
|
||||
| UniversalEventData.oneOf[2] | session/prompt.result.stopReason |
|
||||
| UniversalEventData.oneOf[2].$ref(SessionEndedData) | session/prompt.result.stopReason (+ _meta extension) |
|
||||
| UniversalEventData.oneOf[3] | session/update.params.update |
|
||||
| UniversalEventData.oneOf[3].$ref(ItemEventData) | SessionUpdate (tool_call/tool_call_update/content chunk) |
|
||||
| UniversalEventData.oneOf[4] | session/update.params.update(sessionUpdate=*message_chunk) |
|
||||
| UniversalEventData.oneOf[4].$ref(ItemDeltaData) | ContentChunk |
|
||||
| UniversalEventData.oneOf[5] | JSON-RPC Error |
|
||||
| UniversalEventData.oneOf[5].$ref(ErrorData) | JSON-RPC Error |
|
||||
| UniversalEventData.oneOf[6] | session/request_permission |
|
||||
| UniversalEventData.oneOf[6].$ref(PermissionEventData) | session/request_permission |
|
||||
| UniversalEventData.oneOf[7] | UNIMPLEMENTED |
|
||||
| UniversalEventData.oneOf[7].$ref(QuestionEventData) | UNIMPLEMENTED |
|
||||
| UniversalEventData.oneOf[8] | session/update.params.update._meta["sandbox-agent/unparsed"] (extension) |
|
||||
| UniversalEventData.oneOf[8].$ref(AgentUnparsedData) | session/update.params.update._meta["sandbox-agent/unparsed"] (extension) |
|
||||
| UniversalEventType | session/update.sessionUpdate + JSON-RPC method categories |
|
||||
| UniversalItem | SessionUpdate payload (ContentChunk \| ToolCall \| ToolCallUpdate) |
|
||||
| UniversalItem.content | ContentChunk.content \| ToolCall.content \| ToolCallUpdate.content |
|
||||
| UniversalItem.content[] | ToolCall.content[] \| ToolCallUpdate.content[] |
|
||||
| UniversalItem.content[].$ref(ContentPart) | ToolCallContent \| ContentBlock |
|
||||
| UniversalItem.item_id | ToolCall.toolCallId \| ToolCallUpdate.toolCallId \| _meta["sandbox-agent/itemId"] |
|
||||
| UniversalItem.kind | SessionUpdate.sessionUpdate + ToolKind |
|
||||
| UniversalItem.kind.$ref(ItemKind) | SessionUpdate.sessionUpdate + ToolKind |
|
||||
| UniversalItem.native_item_id | SessionUpdate._meta["sandbox-agent/nativeItemId"] (extension) |
|
||||
| UniversalItem.parent_id | SessionUpdate._meta["sandbox-agent/parentId"] (extension) |
|
||||
| UniversalItem.role | Role (assistant\|user) for message chunks |
|
||||
| UniversalItem.role.allOf[0] | Role |
|
||||
| UniversalItem.role.allOf[0].$ref(ItemRole) | Role (partial) |
|
||||
| UniversalItem.status | ToolCall.status \| ToolCallUpdate.status |
|
||||
| UniversalItem.status.$ref(ItemStatus) | ToolCallStatus |
|
||||
|
||||
## Caveats
|
||||
|
||||
- `UNIMPLEMENTED` means there is no ACP-standard field/method with equivalent semantics in `schema.unstable.json`; implementation would require ACP extension methods (`_...`) and/or `_meta` payloads.
|
||||
- Rows mapped to `_meta[...]` are ACP-compatible extensions, not standard interoperable ACP fields; both sides must agree on names and semantics.
|
||||
- Legacy event polling (`/v1/sessions/{session_id}/events`) has no ACP equivalent; ACP is stream-first via `session/update` notifications over streamable HTTP.
|
||||
- Session lifecycle differs: ACP has `session/new`, `session/load`, `session/resume` (UNSTABLE), and `session/fork` (UNSTABLE), but no standard explicit "close session" method.
|
||||
- Permission handling is request/response (`session/request_permission`) tied to JSON-RPC request IDs; it does not use standalone REST reply endpoints.
|
||||
- Question/answer HITL flow in the old schema has no standard ACP equivalent today (separate from permission prompts).
|
||||
- Agent registry/installation/server health/status APIs are outside ACP core and require separate custom HTTP APIs or ACP extensions.
|
||||
- ACP filesystem methods are client capabilities (`fs/read_text_file`, `fs/write_text_file`) and are text-only; old binary/raw filesystem REST operations remain out of scope for ACP core.
|
||||
- Model and session listing mappings rely on ACP UNSTABLE methods (`session/list`, `session/set_model`, model state in session responses) and may change.
|
||||
- Some old enums do not match ACP enum domains 1:1 (for example `ErrorType`, `SessionEndReason`, `PermissionStatus`); mappings here are best-effort normalization.
|
||||
21
research/acp/rfds-vs-extensions.md
Normal file
21
research/acp/rfds-vs-extensions.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# ACP RFDs vs Extensions
|
||||
|
||||
Status date: 2026-02-10
|
||||
|
||||
| Feature Area | Approach | Short-Term Extension Path | Notes |
|
||||
| ------------------------------------------------------------------------------------------------------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| Generic non-permission HITL question/input flow (agent request + client response) | RFD | `_sandboxagent/session/request_question` request + structured JSON-RPC response (`answered/rejected/cancelled`). | Needs standard ACP request/response shape beyond permission prompts. |
|
||||
| Standard model listing support (including pre-session discovery and/or stable session model state) | RFD | `_sandboxagent/session/list_models` response with `availableModels`, `currentModelId`, plus variant fields (`defaultVariant`, `variants[]`) in payload or `_meta`. | Includes model variant semantics (`defaultVariant`, `variants[]`). |
|
||||
| Stabilize `session/list` | RFD | Use ACP unstable `session/list` directly (no custom extension alias). | Currently unstable ACP method. |
|
||||
| Stabilize `session/fork` | RFD | Use ACP unstable `session/fork` directly (no custom extension alias). | Currently unstable ACP method. |
|
||||
| Stabilize `session/resume` | RFD | Use ACP unstable `session/resume` directly (no custom extension alias). | Currently unstable ACP method. |
|
||||
| Stabilize `session/set_model` | RFD | Use ACP unstable `session/set_model` directly; fallback to `session/set_config_option` where available. | Currently unstable ACP method. |
|
||||
| Standard session metadata-at-creation semantics (portable title/tags-style hints) | RFD | Use `session/new.params._meta["sandboxagent.dev"]` for create-time metadata; optional `_sandboxagent/session/set_metadata` for post-create updates. | Reduces custom `_meta` usage for common metadata. |
|
||||
| Sandbox Agent robust HTTP filesystem API | Extension | Already extension (`/v2/fs/*` custom HTTP surface). | Separate custom HTTP API; not ACP core agent-session protocol. |
|
||||
| `skills` payload and behavior | Extension | Already extension via `_meta["sandboxagent.dev"].skills` and optional `_sandboxagent/session/set_metadata`. | Product-specific behavior. |
|
||||
| Client-requested session alias (`requestedSessionId`) behavior | Extension | Already extension via `_meta["sandboxagent.dev"].requestedSessionId`. | Product-specific metadata/ID behavior. |
|
||||
| Agent version pin/hint behavior during bootstrap/session creation | Extension | Already extension via `_meta["sandboxagent.dev"].agentVersionRequested`. | Product-specific runtime selection behavior. |
|
||||
| Agent control-plane inventory/install state (`list/install/credentialsAvailable/installed/path`) | Extension | Already extension as custom control-plane HTTP (`/v2/agents`, `/v2/agents/{agent}/install`). | Control-plane functionality outside ACP core. |
|
||||
| Runtime daemon/process status and related capabilities (`serverStatus`, `sharedProcess`, `commandExecution`, `errorEvents`) | Extension | Already extension as control-plane fields (`/v2/agents`, `/v2/health`) and capability `_meta` hints. | Runtime/orchestrator-specific state. |
|
||||
| Sandbox Agent control-plane APIs (`/v2/health`, `/v2/agents`, `/v2/agents/{agent}/install`) | Extension | Already extension (custom non-ACP API surface). | Custom non-ACP HTTP surface. |
|
||||
| OpenCode compatibility bridging behavior (`/opencode/*`) | Extension | Already extension as product compatibility bridge layer. | Product compatibility layer, not ACP core protocol. |
|
||||
366
research/acp/spec.md
Normal file
366
research/acp/spec.md
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
# V2 Spec: ACP Over HTTP
|
||||
|
||||
## 0) Delete/Remove First
|
||||
|
||||
Before implementing v2, remove in-house protocol files and remove v1 API behavior as documented in:
|
||||
|
||||
- `research/acp/00-delete-first.md`
|
||||
- `research/acp/v1-schema-to-acp-mapping.md` (endpoint/event-by-event conversion target)
|
||||
|
||||
This is mandatory to prevent dual-protocol drift.
|
||||
|
||||
## 1) Goals
|
||||
|
||||
- v2 is intentionally breaking and ACP-native.
|
||||
- Internal runtime uses ACP end-to-end (no custom universal event schema).
|
||||
- Existing agent managers are replaced with ACP agent process runtimes.
|
||||
- v1 API is completely removed; all `/v1/*` requests return explicit removed errors.
|
||||
- OpenCode <-> ACP support is preserved as a product requirement, but implemented in a separate step after ACP core is stable.
|
||||
|
||||
## 2) Non-goals for first v2 cut
|
||||
|
||||
- No guarantee of v1 endpoint compatibility.
|
||||
- No v1 compatibility layer in the initial v2 release.
|
||||
- No OpenCode compatibility during ACP core bring-up (`/opencode/*` is disabled until the dedicated bridge step).
|
||||
- No in-house universal event format.
|
||||
|
||||
## 3) Protocol baseline
|
||||
|
||||
Use ACP v1 schema from:
|
||||
|
||||
- `~/misc/acp-docs/schema/schema.json`
|
||||
- `~/misc/acp-docs/schema/meta.json`
|
||||
- `research/acp/v1-schema-to-acp-mapping.md` (required for preserving v1 feature coverage during migration)
|
||||
|
||||
Supported agent methods (minimum):
|
||||
|
||||
- `initialize`, `authenticate` (if needed), `session/new`, `session/prompt`, `session/cancel`
|
||||
|
||||
Supported client methods (minimum):
|
||||
|
||||
- `session/request_permission`
|
||||
|
||||
Included unstable ACP methods for v2 profile:
|
||||
|
||||
- `session/list`
|
||||
- `session/fork`
|
||||
- `session/resume`
|
||||
- `session/set_model`
|
||||
- `$/cancel_request`
|
||||
|
||||
Phase-1 optional methods (still optional per agent process capability):
|
||||
|
||||
- `fs/read_text_file`, `fs/write_text_file`
|
||||
- `terminal/*`
|
||||
- `session/load`, `session/set_mode`, `session/set_config_option`
|
||||
|
||||
Sandbox Agent ACP extension methods currently implemented:
|
||||
|
||||
- `_sandboxagent/session/detach`
|
||||
- `_sandboxagent/session/list_models`
|
||||
- `_sandboxagent/session/set_metadata`
|
||||
- `_sandboxagent/session/request_question` (agent -> client request pattern)
|
||||
- `_sandboxagent/session/terminate`
|
||||
- `_sandboxagent/session/ended` (runtime -> client notification)
|
||||
|
||||
## 4) Transport: ACP over HTTP (repo-specific draft)
|
||||
|
||||
ACP streamable HTTP is draft upstream, so this spec defines a concrete transport that stays close to current ACP transport guidance and JSON-RPC semantics.
|
||||
|
||||
### 4.1 Endpoints
|
||||
|
||||
- `POST /v2/rpc`
|
||||
- `GET /v2/rpc` (SSE stream, `Accept: text/event-stream`)
|
||||
- `DELETE /v2/rpc` (explicit connection close)
|
||||
|
||||
### 4.2 Connection model
|
||||
|
||||
- Each ACP HTTP connection maps to a logical client connection id (`X-ACP-Connection-Id`).
|
||||
- Agent processes are shared per `AgentId` (one backend process per agent type on a server), not per HTTP connection.
|
||||
- Session inventory is server-global in memory across connections; `session/list` returns this aggregated inventory.
|
||||
- Connection identity is `X-ACP-Connection-Id` header.
|
||||
- First `initialize` request may omit `X-ACP-Connection-Id` and must include `params._meta["sandboxagent.dev"].agent`.
|
||||
- Server ensures backend exists for that agent, creates connection, returns `X-ACP-Connection-Id` in response headers.
|
||||
- All subsequent `POST /v2/rpc` and `GET /v2/rpc` requests must include `X-ACP-Connection-Id`.
|
||||
- `DELETE /v2/rpc` with `X-ACP-Connection-Id` detaches/closes only the transport connection and releases connection-scoped resources.
|
||||
- `DELETE /v2/rpc` does not terminate the session or agent process. Session termination is explicit via `_sandboxagent/session/terminate`.
|
||||
|
||||
### 4.3 Message routing
|
||||
|
||||
- Client -> agent requests/notifications: sent as JSON-RPC payloads to `POST /v2/rpc`.
|
||||
- Agent -> client notifications/requests: delivered on `GET /v2/rpc` SSE stream as JSON-RPC envelopes.
|
||||
- Client replies to agent-initiated requests by POSTing JSON-RPC responses to `POST /v2/rpc`.
|
||||
|
||||
### 4.4 SSE framing
|
||||
|
||||
- `event: message`
|
||||
- `id: <monotonic-sequence>`
|
||||
- `data: <single JSON-RPC object>`
|
||||
|
||||
Keepalive:
|
||||
|
||||
- SSE comment heartbeat every 15s.
|
||||
|
||||
Resume:
|
||||
|
||||
- `Last-Event-ID` accepted for best-effort replay from in-memory ring buffer.
|
||||
|
||||
### 4.5 HTTP status and errors
|
||||
|
||||
- JSON-RPC request success: HTTP 200 with JSON-RPC response object.
|
||||
- JSON-RPC notification accepted: HTTP 202, empty body.
|
||||
- Invalid envelope: HTTP 400 with `application/problem+json`.
|
||||
- Unknown connection: HTTP 404 with `application/problem+json`.
|
||||
- Server timeout waiting on agent process response: HTTP 504 with `application/problem+json`.
|
||||
- Successful `DELETE /v2/rpc`: HTTP 204.
|
||||
- Repeated `DELETE /v2/rpc` on an already-closed connection: HTTP 204 (idempotent close).
|
||||
- All `/v1/*` endpoints: HTTP 410 with `application/problem+json` and message `v1 API removed; use /v2`.
|
||||
|
||||
Note: ACP method-level failures still return JSON-RPC error objects inside 200 responses.
|
||||
|
||||
### 4.6 Ordering and concurrency
|
||||
|
||||
- Per-connection outbound SSE order is preserved.
|
||||
- JSON-RPC `id` is opaque and passed through unchanged.
|
||||
- Multiple in-flight requests are allowed.
|
||||
|
||||
### 4.7 Security
|
||||
|
||||
- Reuse existing bearer token auth middleware.
|
||||
- Validate bearer auth at request time for `/v2/*` routes when configured.
|
||||
- ACP runtime connection ids are in-memory server ids and are not additionally principal-scoped inside runtime.
|
||||
- Do not expose agent process stderr on stdout channel.
|
||||
|
||||
### 4.8 Field research alignment (2026-02-10)
|
||||
|
||||
Based on current ACP community implementations/discussion:
|
||||
|
||||
- Streamable HTTP and WebSocket are both being piloted.
|
||||
- Streamable HTTP implementations are converging on MCP-like request patterns while keeping ACP JSON-RPC payloads.
|
||||
- WebSocket implementations report simpler handling for bidirectional/server-initiated ACP traffic.
|
||||
|
||||
Decision for this repo:
|
||||
|
||||
- v2 public transport remains Streamable HTTP (`POST`/`GET` SSE over `/v2/rpc`) as the canonical contract.
|
||||
- WebSocket transport is not part of initial v2 surface; consider later only if HTTP profile proves insufficient operationally.
|
||||
|
||||
Reference:
|
||||
|
||||
- `research/acp/acp-over-http-findings.md`
|
||||
|
||||
## 5) Agent process runtime and install model
|
||||
|
||||
## 5.1 Runtime
|
||||
|
||||
Replace custom per-agent protocol parsers with one ACP agent process process contract:
|
||||
|
||||
- Spawn ACP agent process binary (stdio JSON-RPC).
|
||||
- Bridge stdio <-> internal ACP client dispatcher.
|
||||
- No agent-specific JSON parsing in server core.
|
||||
|
||||
## 5.2 Installers
|
||||
|
||||
Current auto-installer installs native CLIs. v2 installer must install:
|
||||
|
||||
- native agent binary (if needed by agent process)
|
||||
- ACP agent process binary required for that agent
|
||||
|
||||
Add a manifest-driven mapping (new file to create in implementation phase):
|
||||
|
||||
- `claude`: agent process `claude-code-acp`
|
||||
- `codex`: agent process `codex-acp`
|
||||
- `opencode`: native ACP mode (agent process optional)
|
||||
- `amp`: pending decision (official agent process required or unsupported in v2 initial release)
|
||||
|
||||
## 5.3 ACP Registry install instructions
|
||||
|
||||
Yes, this must be handled.
|
||||
|
||||
V2 installer/docs must include install instructions sourced from ACP registry metadata where available, with explicit fallback for non-registry agent processes.
|
||||
|
||||
Requirements:
|
||||
|
||||
- Maintain a local agent process manifest with:
|
||||
- registry slug (if present)
|
||||
- agent process package/repo source
|
||||
- native agent dependency
|
||||
- supported platform matrix
|
||||
- install verification command
|
||||
- Prefer ACP registry source of truth when agent process is published there.
|
||||
- If agent process is not in registry, use pinned fallback source and mark as `non_registry`.
|
||||
- Support `SANDBOX_AGENT_ACP_REGISTRY_URL` override for controlled/test environments.
|
||||
- Generate user-facing install instructions from this manifest (do not hand-maintain per-agent docs).
|
||||
- Expose install provenance in API/CLI (`registry` vs `fallback`).
|
||||
|
||||
Output surfaces:
|
||||
|
||||
- `GET /v2/agents`: include agent process install source + verification status.
|
||||
- `POST /v2/agents/{agent}/install`: return concrete installed artifacts and source provenance.
|
||||
|
||||
## 5.4 Install commands and lazy agent process install
|
||||
|
||||
This must match current ergonomics where installs can be explicit or automatic.
|
||||
|
||||
Explicit install interfaces:
|
||||
|
||||
- API: `POST /v2/agents/{agent}/install`
|
||||
- CLI (v2 surface): `sandbox-agent api v2 agents install <agent> [--reinstall] [--agent-version <v>] [--agent process-version <v>]`
|
||||
|
||||
Lazy install behavior (default on):
|
||||
|
||||
- Trigger point: first ACP bootstrap request (`initialize`) on `/v2/rpc` with `params._meta["sandboxagent.dev"].agent`.
|
||||
- If required binaries are missing, server installs:
|
||||
- ACP agent process
|
||||
- native agent binary if agent process requires it
|
||||
- Then server starts the agent process process and continues normal ACP handshake.
|
||||
|
||||
Operational requirements:
|
||||
|
||||
- Per-agent install lock to prevent duplicate concurrent downloads.
|
||||
- Idempotent install response when artifacts already exist.
|
||||
- Clear provenance in result (`registry` vs `fallback`) plus concrete artifact versions.
|
||||
- Config switch to disable lazy install (`require_preinstall=true`) for controlled environments.
|
||||
|
||||
## 6) Public v2 API shape
|
||||
|
||||
Expose ACP directly, not custom session endpoints.
|
||||
|
||||
- `POST /v2/rpc`: transport write path
|
||||
- `GET /v2/rpc`: transport read path (SSE)
|
||||
|
||||
Non-ACP endpoints retained in v2:
|
||||
|
||||
- `GET /v2/health`
|
||||
- `GET /v2/agents` (capabilities + install status)
|
||||
- `POST /v2/agents/{agent}/install`
|
||||
- `GET /v2/sessions`
|
||||
- `GET /v2/sessions/{id}`
|
||||
- `GET /v2/fs/file`
|
||||
- `PUT /v2/fs/file`
|
||||
- `POST /v2/fs/upload-batch`
|
||||
- `GET /ui/` (Inspector UI shell)
|
||||
|
||||
Agent discovery note:
|
||||
|
||||
- Do not add standalone `/v2/agents/{agent}/models` or `/v2/agents/{agent}/modes` endpoints.
|
||||
- Expose optional `models`/`modes` properties on agent response payloads when the agent is installed.
|
||||
|
||||
Legacy endpoints retained only as removals:
|
||||
|
||||
- `ALL /v1/*` return HTTP 410 with a stable "v1 removed" error body.
|
||||
- `/opencode/*` is commented out/disabled until Phase 7 and is expected to be broken during ACP core bring-up.
|
||||
|
||||
Everything related to prompting/sessions/permissions/tools happens through ACP JSON-RPC messages.
|
||||
|
||||
## 6.3 Filesystem Boundary (Intentional)
|
||||
|
||||
Sandbox Agent keeps a separate host-owned filesystem API from ACP native filesystem methods.
|
||||
|
||||
Rationale:
|
||||
|
||||
- ACP `fs/*` methods are agent-protocol capabilities and can vary by agent implementation.
|
||||
- Sandbox Agent filesystem HTTP endpoints are host/runtime capabilities and should behave consistently across all agents.
|
||||
- Large binary transfers (raw file read/write and tar upload) need streaming-friendly HTTP behavior; ACP JSON-RPC envelopes are not suitable for super-large binary payload transport.
|
||||
- `GET /v2/fs/file` specifically benefits from HTTP response streaming for large reads.
|
||||
|
||||
For this reason, `GET /v2/fs/file`, `PUT /v2/fs/file`, and `POST /v2/fs/upload-batch` remain dedicated HTTP endpoints even as other static control APIs migrate to ACP extensions.
|
||||
|
||||
Parallel ACP compatibility is still supported:
|
||||
|
||||
- Keep ACP extension variants in parallel for these operations.
|
||||
- ACP and HTTP variants should call into the same underlying filesystem service logic so behavior remains consistent.
|
||||
- ACP variants are not intended for very large file transfer; SDK defaults should prefer HTTP for these methods.
|
||||
|
||||
## 6.1 TypeScript SDK integration (mandatory)
|
||||
|
||||
Use the existing ACP TypeScript SDK (`@agentclientprotocol/sdk`) inside our SDK implementation.
|
||||
|
||||
Rules:
|
||||
|
||||
- Do not build a second in-house ACP protocol implementation in `sdks/typescript`.
|
||||
- Wrap and embed ACP SDK primitives (`ClientSideConnection`/transport handling) in our SDK surface.
|
||||
- Implement our own ACP-over-HTTP transport agent process for the SDK because the official ACP client SDK does not currently provide our required Streamable HTTP client behavior out of the box.
|
||||
- Keep our SDK focused on:
|
||||
- auth/token handling
|
||||
- endpoint/bootstrap convenience
|
||||
- spawn/embedded server ergonomics
|
||||
- product-specific helper APIs
|
||||
- ACP message framing, JSON-RPC lifecycle, and protocol object modeling should come from upstream ACP SDK.
|
||||
|
||||
## 6.2 Inspector (mandatory)
|
||||
|
||||
The inspector must be ACP-native in v2.
|
||||
|
||||
- Replace v1 session/event calls in inspector with ACP-over-HTTP connection flow (`initialize`, `session/new`, `session/prompt`, streamed `session/update`).
|
||||
- Add inspector support for rendering raw ACP envelopes and decoded session updates.
|
||||
- Keep inspector route at `/ui/`; remove dependency on v1 REST session endpoints.
|
||||
- Inspector end-to-end verification is mandatory via `agent-browser` automation (not only unit/integration tests).
|
||||
|
||||
## 7) Data model changes (breaking)
|
||||
|
||||
Removed from public contract:
|
||||
|
||||
- `UniversalEvent`
|
||||
- `UniversalItem`
|
||||
- custom `permission.reply` and `question.reply` REST endpoints
|
||||
- v1 `sessions/*` REST resources
|
||||
|
||||
Added:
|
||||
|
||||
- raw ACP JSON-RPC envelopes over HTTP
|
||||
- explicit connection identity (`X-ACP-Connection-Id`)
|
||||
|
||||
## 8) Test contract for v2
|
||||
|
||||
Consolidated must-have suites (duplicates collapsed):
|
||||
|
||||
- ACP protocol conformance (JSON-RPC + ACP schema/semantics)
|
||||
- Transport contract (`/v2/rpc` POST/SSE routing, ordering, replay, errors)
|
||||
- End-to-end agent process matrix (includes core turn flow, cancel, HITL, streaming)
|
||||
- Installer suite (explicit + lazy install, registry/fallback provenance)
|
||||
- Security/auth isolation
|
||||
- TypeScript SDK end-to-end (embedded + server mode, embedding `@agentclientprotocol/sdk`)
|
||||
- v1 removal contract suite (`/v1/*` => HTTP 410 + stable error payload)
|
||||
- Inspector ACP suite executed with `agent-browser` (ACP session flow + streaming render correctness)
|
||||
- OpenCode <-> ACP bridge suite (dedicated later phase)
|
||||
|
||||
No synthetic protocol fixtures. Use real agent processes in integration tests.
|
||||
|
||||
Minimum required `agent-browser` inspector coverage:
|
||||
|
||||
1. Open `/ui/`, spawn an agent/session, send one message, and verify a response is rendered.
|
||||
|
||||
Current automation entrypoint:
|
||||
|
||||
- `frontend/packages/inspector/tests/agent-browser.e2e.sh`
|
||||
- `server/packages/sandbox-agent/tests/v2_agent_process_matrix.rs` (deterministic agent process matrix smoke + JSON-RPC conformance checks)
|
||||
|
||||
## 9) Open questions to resolve before implementation lock
|
||||
|
||||
- Amp agent process availability and support level for v2 launch.
|
||||
- Whether additional non-binary filesystem endpoints remain HTTP or migrate to ACP extensions after initial cut.
|
||||
|
||||
## 10) Deferred Dedicated Step: OpenCode <-> ACP
|
||||
|
||||
- During ACP core implementation, `/opencode/*` is commented out/disabled.
|
||||
- After ACP core is stable, complete the dedicated OpenCode <-> ACP bridge step and re-enable `/opencode/*`.
|
||||
- Mark the step complete only after dedicated integration tests pass.
|
||||
|
||||
## 11) Companion Docs
|
||||
|
||||
- `research/acp/v1-schema-to-acp-mapping.md` (normative 1:1 endpoint/event mapping)
|
||||
- `research/acp/migration-steps.md` (execution order and rollout steps)
|
||||
- `research/acp/todo.md` (phase checklist and validation tracker)
|
||||
- `research/acp/acp-over-http-findings.md` (community transport findings and decision context)
|
||||
- `research/acp/friction.md` (ongoing issue/decision log)
|
||||
- `docs/quickstart.mdx`, `docs/cli.mdx`, `docs/sdks/typescript.mdx` (external migration and rollout guidance)
|
||||
|
||||
## 12) Documentation Updates (mandatory)
|
||||
|
||||
When implementing this spec, update docs in the same change set:
|
||||
|
||||
- API reference/openapi docs for v2 and `/v1/*` removal semantics
|
||||
- `docs/cli.mdx` for v2 ACP and removed v1 commands
|
||||
- `docs/inspector.mdx` for ACP-based inspector behavior
|
||||
- SDK docs (`docs/sdks/typescript.mdx`) for ACP-over-HTTP transport usage
|
||||
- Any OpenCode compatibility docs that reference `/opencode/*`
|
||||
169
research/acp/todo.md
Normal file
169
research/acp/todo.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# ACP v2 Migration TODO
|
||||
|
||||
Source docs:
|
||||
- `research/acp/spec.md`
|
||||
- `research/acp/migration-steps.md`
|
||||
- `research/acp/00-delete-first.md`
|
||||
- `research/acp/v1-schema-to-acp-mapping.md`
|
||||
- `research/acp/friction.md`
|
||||
|
||||
Progress rule:
|
||||
- [ ] Do not start the next phase until current phase gate is green in local + CI.
|
||||
- [x] Log blockers/decisions in `research/acp/friction.md` during implementation.
|
||||
|
||||
## Phase 1: Teardown
|
||||
|
||||
Implementation:
|
||||
- [x] Delete in-house protocol files/docs listed in `research/acp/00-delete-first.md`.
|
||||
- [x] Remove deleted-crate deps from workspace `Cargo.toml` files.
|
||||
- [x] Remove `/v1` route registration.
|
||||
- [x] Add unified `/v1/*` removed handler (HTTP 410 + `application/problem+json`).
|
||||
- [x] Remove/disable CLI `api` commands that target `/v1`.
|
||||
- [x] Comment out/disable `/opencode/*` during ACP core bring-up.
|
||||
|
||||
Validation gate:
|
||||
- [x] Project builds with v1 protocol code removed.
|
||||
- [x] No references to `sandbox-agent-universal-agent-schema` remain.
|
||||
- [x] `/v1/*` returns explicit "v1 removed" error (HTTP 410).
|
||||
- [x] `/opencode/*` returns disabled/unavailable response.
|
||||
|
||||
## Phase 2: ACP Core Runtime
|
||||
|
||||
Implementation:
|
||||
- [x] Add ACP runtime module + router integration.
|
||||
- [x] Implement agent process process manager (spawn/supervise baseline).
|
||||
- [x] Implement JSON-RPC bridge (`POST`/SSE <-> agent process stdio).
|
||||
- [x] Add connection registry keyed by `X-ACP-Connection-Id`.
|
||||
- [x] Implement unstable methods in v2 profile: `session/list`, `session/fork`, `session/resume`, `session/set_model`, `$/cancel_request`.
|
||||
- [x] Implement explicit close path: `DELETE /v2/rpc`.
|
||||
|
||||
Validation gate:
|
||||
- [x] End-to-end ACP flow over `/v2/rpc` (request/response + streamed notifications).
|
||||
- [x] `session/cancel` behavior test passes.
|
||||
- [x] HITL request/response round-trip test passes.
|
||||
- [x] SSE ordering and `Last-Event-ID` replay test passes.
|
||||
- [x] `DELETE /v2/rpc` idempotent double-close test passes.
|
||||
- [x] Unstable method tests pass for agent processes that advertise support (mock covered).
|
||||
|
||||
## Phase 3: Installer Refactor
|
||||
|
||||
Implementation:
|
||||
- [x] Replace agent-specific spawn contracts with agent process-centric spawn.
|
||||
- [x] Add agent process install manifests + downloader logic.
|
||||
- [x] Keep native agent installs where agent process depends on local CLI.
|
||||
- [x] Add install verification command per agent process.
|
||||
- [x] Integrate ACP registry metadata + fallback sources.
|
||||
- [x] Expose install provenance (`registry` vs `fallback`) in API/CLI.
|
||||
- [x] Implement lazy install on first `/v2/rpc` initialize.
|
||||
- [x] Add per-agent install lock + idempotent install results.
|
||||
- [x] Add config switch to disable lazy install for preprovisioned envs (`SANDBOX_AGENT_REQUIRE_PREINSTALL`).
|
||||
- [ ] Fill out installers for all ACP registry agents (expand `AgentId` + per-agent installer mappings).
|
||||
|
||||
Validation gate:
|
||||
- [x] Explicit install command tests pass for each supported agent.
|
||||
- [x] Lazy install on first ACP initialize test passes (deterministic local-registry coverage added).
|
||||
- [x] Reinstall/version/provenance assertions pass.
|
||||
- [ ] Add integration coverage that every ACP registry agent has a corresponding installer mapping in `agent-management`.
|
||||
|
||||
## Phase 4: v2 HTTP API
|
||||
|
||||
Implementation:
|
||||
- [x] Mount `POST /v2/rpc` and `GET /v2/rpc` (SSE).
|
||||
- [x] Mount `DELETE /v2/rpc` close endpoint.
|
||||
- [x] Add `GET /v2/health`, `GET /v2/agents`, `POST /v2/agents/{agent}/install`.
|
||||
- [x] Integrate auth on ACP client lifecycle.
|
||||
- [x] Keep `/ui/` and migrate inspector backend calls to ACP v2 transport.
|
||||
- [x] Remove v1 OpenAPI surface from generated docs contract.
|
||||
|
||||
Validation gate:
|
||||
- [x] Contract tests for `/v2` endpoints pass.
|
||||
- [x] Auth tests pass (valid/missing/invalid token).
|
||||
- [x] `/v1/*` removal contract test passes (HTTP 410 + stable payload).
|
||||
- [x] Inspector ACP `agent-browser` flow test passes.
|
||||
- [x] `DELETE /v2/rpc` close contract tests pass.
|
||||
- [x] Error mapping tests are complete for every documented error path.
|
||||
|
||||
## Phase 5: SDK and CLI v2
|
||||
|
||||
Implementation:
|
||||
- [x] Embed `@agentclientprotocol/sdk` in `sdks/typescript`.
|
||||
- [x] Implement custom ACP-over-HTTP transport agent process in our SDK.
|
||||
- [x] Wire inspector frontend client to ACP-over-HTTP primitives.
|
||||
- [x] Add CLI commands for raw ACP envelopes + streaming ACP messages.
|
||||
- [x] Remove or hard-fail v1-only SDK/CLI methods (`v1 removed`).
|
||||
- [x] Regenerate docs for v2 ACP contract.
|
||||
|
||||
Validation gate:
|
||||
- [x] TypeScript SDK end-to-end tests pass in embedded mode.
|
||||
- [x] TypeScript SDK end-to-end tests pass in server mode.
|
||||
- [x] Inspector end-to-end `agent-browser` tests pass using ACP-over-HTTP.
|
||||
- [x] Add explicit parity test asserting `ClientSideConnection` usage contract.
|
||||
|
||||
## Phase 6: Test and Rollout
|
||||
|
||||
Implementation:
|
||||
- [x] Replace v1 HTTP/session tests with ACP transport contract tests (core server + SDK).
|
||||
- [x] Add smoke tests per supported agent process (claude/codex/opencode covered with deterministic ACP agent process stubs).
|
||||
- [x] Add canary docs + migration notes.
|
||||
- [x] Update docs for v2 ACP, `/v1/*` removal, inspector ACP behavior, and SDK usage.
|
||||
- [x] Keep `/v1/*` hard-removed (HTTP 410).
|
||||
|
||||
Validation gate:
|
||||
- [x] Full agent process matrix is green.
|
||||
- [x] Install + prompt + stream smoke tests pass for each supported agent process.
|
||||
- [x] Inspector `agent-browser` suite runs in CI path.
|
||||
- [ ] Docs updates are published with rollout.
|
||||
|
||||
Notes:
|
||||
- Remaining unchecked rollout items depend on docs publishing workflow outside this repo change set.
|
||||
- Real credentialed agent process matrix runs are still environment-dependent; deterministic agent process matrix coverage is now in CI.
|
||||
|
||||
## Phase 7: OpenCode <-> ACP Bridge (Dedicated Step)
|
||||
|
||||
Implementation:
|
||||
- [x] Keep `/opencode/*` disabled through Phases 1-6.
|
||||
- [ ] Implement OpenCode <-> ACP bridge on top of v2 ACP runtime.
|
||||
- [ ] Re-enable `server/packages/sandbox-agent/src/opencode_compat.rs` routes/tests.
|
||||
- [ ] Add dedicated integration tests for OpenCode SDK/TUI flows through ACP v2 internals.
|
||||
|
||||
Validation gate:
|
||||
- [ ] OpenCode compatibility suite passes against ACP-backed implementation.
|
||||
- [ ] Regression tests confirm no dependency on removed in-house protocol runtime.
|
||||
|
||||
## Consolidated Test Suites (Must-Have)
|
||||
|
||||
- [x] ACP protocol conformance (beyond mock baseline).
|
||||
- [x] `/v2/rpc` transport contract.
|
||||
- [x] End-to-end agent process matrix (core + cancel + HITL + streaming).
|
||||
- [x] Installer suite (explicit + lazy + provenance).
|
||||
- [x] Security/auth isolation.
|
||||
- [x] TypeScript SDK end-to-end (embedded + server).
|
||||
- [x] v1 removal contract (`/v1/*` -> HTTP 410).
|
||||
- [x] Inspector ACP suite (`agent-browser`).
|
||||
- [ ] OpenCode <-> ACP bridge suite (Phase 7).
|
||||
|
||||
## Architecture: Connection vs Session Model
|
||||
|
||||
- [x] Align runtime with multi-session ACP expectations while keeping one backend process per `AgentId`.
|
||||
- ACP HTTP connections are logical client channels; server sessions are globally visible via aggregated `session/list`.
|
||||
- Backend process ownership is per agent type (shared per server), not per client connection.
|
||||
- Added connection-level session detachment extension `_sandboxagent/session/detach`.
|
||||
- Documented updated model in `research/acp/spec.md` and `research/acp/friction.md`.
|
||||
|
||||
## Newly discovered follow-ups
|
||||
|
||||
- [x] Add dedicated regression for `Last-Event-ID` handling in CLI `api acp stream`.
|
||||
- [x] Add explicit test for `SANDBOX_AGENT_REQUIRE_PREINSTALL=true` behavior.
|
||||
- [x] Improve server build-script invalidation for inspector embedding (avoid manual touch workaround when `dist/` appears after initial build).
|
||||
- [ ] Integrate agent server logs into v2 observability surfaces (agent process/process logs available via control-plane and inspector), with redaction and end-to-end tests.
|
||||
|
||||
## Inspector Frontend Parity Follow-ups
|
||||
|
||||
- [ ] TODO: Implement session `permissionMode` preconfiguration in inspector ACP flow.
|
||||
- [ ] TODO: Implement session `variant` preconfiguration in inspector ACP flow.
|
||||
- [ ] TODO: Implement session `skills` source configuration in inspector ACP flow.
|
||||
- [ ] TODO: Implement question request/reply/reject flow in inspector ACP flow.
|
||||
- [ ] TODO: Implement agent mode discovery before session creation (replace cached/empty fallback).
|
||||
- [ ] TODO: Implement agent model discovery before session creation (replace cached/empty fallback).
|
||||
- [ ] TODO: Replace inspector-local session list with server/global ACP-backed session inventory.
|
||||
- [ ] TODO: Replace synthesized inspector event history with canonical ACP-backed history model.
|
||||
221
research/acp/ts-client.md
Normal file
221
research/acp/ts-client.md
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# TypeScript Client Rewrite Spec (ACP HTTP Client + Sandbox Agent SDK)
|
||||
|
||||
## Status
|
||||
- Draft.
|
||||
- Captures confirmed decisions and server-verified contracts before implementation.
|
||||
|
||||
## Goals
|
||||
- Split TypeScript clients into:
|
||||
1. `acp-http-client`: protocol-pure ACP-over-HTTP transport/client.
|
||||
2. `sandbox-agent` SDK: Sandbox Agent wrapper that hides ACP terminology and applies Sandbox-specific metadata/extensions.
|
||||
- Make the Sandbox Agent SDK API as simple as creating a client and connecting once.
|
||||
- Remove ACP-facing API from `sandbox-agent` public surface.
|
||||
|
||||
## Confirmed Product Decisions
|
||||
- Dedicated protocol package name: `acp-http-client`.
|
||||
- `acp-http-client` must implement ACP HTTP protocol "to the T" and include no Sandbox-specific metadata/extensions.
|
||||
- Sandbox SDK public constructor pattern: `new SandboxAgentClient(...)`.
|
||||
- Sandbox SDK auto-connects by default, but supports disabling auto-connect.
|
||||
- ACP-related SDK calls must fail if `.connect()` has not been called.
|
||||
- After `.disconnect()`, ACP-related SDK calls must fail until reconnected.
|
||||
- A `SandboxAgentClient` instance can hold at most one active ACP connection.
|
||||
- No API for creating multiple ACP clients per wrapper instance.
|
||||
- ACP terminology should not appear in Sandbox SDK public API/docs.
|
||||
- Sandbox SDK should be a thin conversion layer on top of ACP protocol, mainly for metadata/event conversion.
|
||||
- Existing ACP-facing methods in `sandbox-agent` are removed (full rewrite).
|
||||
- Non-ACP HTTP helpers remain in `sandbox-agent` (health/agents/install/fs/etc).
|
||||
|
||||
## Server-Verified v2 ACP Contract
|
||||
|
||||
### HTTP endpoints and headers
|
||||
- Endpoints:
|
||||
1. `POST /v2/rpc`
|
||||
2. `GET /v2/rpc` (SSE)
|
||||
3. `DELETE /v2/rpc`
|
||||
- Headers:
|
||||
1. `x-acp-connection-id` for existing connection usage.
|
||||
2. `Last-Event-ID` for SSE replay.
|
||||
3. Agent selection is in payload metadata: `params._meta["sandboxagent.dev"].agent`.
|
||||
- Sources:
|
||||
1. `server/packages/sandbox-agent/src/router.rs:862`
|
||||
2. `server/packages/sandbox-agent/src/router.rs:913`
|
||||
3. `server/packages/sandbox-agent/src/router.rs:948`
|
||||
4. `server/packages/sandbox-agent/src/acp_runtime/mod.rs:26`
|
||||
5. `server/packages/sandbox-agent/src/acp_runtime/mod.rs:27`
|
||||
|
||||
### Custom `_sandboxagent/*` methods/events currently implemented
|
||||
- Request methods handled in runtime:
|
||||
1. `_sandboxagent/session/detach`
|
||||
2. `_sandboxagent/session/terminate`
|
||||
3. `_sandboxagent/session/list_models`
|
||||
4. `_sandboxagent/session/set_metadata`
|
||||
- Notification methods handled in runtime:
|
||||
1. `_sandboxagent/session/detach`
|
||||
2. `_sandboxagent/session/terminate`
|
||||
3. `_sandboxagent/session/set_metadata`
|
||||
- Runtime notifications:
|
||||
1. `_sandboxagent/session/ended`
|
||||
2. `_sandboxagent/agent/unparsed`
|
||||
- Sources:
|
||||
1. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:3`
|
||||
2. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:4`
|
||||
3. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:5`
|
||||
4. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:6`
|
||||
5. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:7`
|
||||
6. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:8`
|
||||
7. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:11`
|
||||
8. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:30`
|
||||
9. `server/packages/sandbox-agent/src/acp_runtime/mod.rs:1496`
|
||||
10. `server/packages/sandbox-agent/src/acp_runtime/backend.rs:95`
|
||||
|
||||
### Custom extension capability advertisement
|
||||
- Injected into `initialize` response at:
|
||||
- `result.agentCapabilities._meta["sandboxagent.dev"].extensions`
|
||||
- Includes booleans and `methods` array for extension availability.
|
||||
- Source:
|
||||
1. `server/packages/sandbox-agent/src/acp_runtime/ext_meta.rs:32`
|
||||
2. `server/packages/sandbox-agent/src/acp_runtime/ext_meta.rs:55`
|
||||
3. `server/packages/sandbox-agent/tests/v2_api/acp_extensions.rs:3`
|
||||
|
||||
## Server-Verified `_meta["sandboxagent.dev"]` Behavior
|
||||
|
||||
### Namespace definition
|
||||
- Canonical metadata namespace key: `sandboxagent.dev`.
|
||||
- Source:
|
||||
1. `server/packages/sandbox-agent/src/acp_runtime/ext_meta.rs:4`
|
||||
|
||||
### Inbound metadata ingestion
|
||||
- `session/new` reads `_meta["sandboxagent.dev"]` as map and stores it.
|
||||
- Source:
|
||||
1. `server/packages/sandbox-agent/src/acp_runtime/mod.rs:610`
|
||||
2. `server/packages/sandbox-agent/src/acp_runtime/ext_meta.rs:21`
|
||||
|
||||
### Metadata mutation extension
|
||||
- `_sandboxagent/session/set_metadata` accepts either:
|
||||
1. `params.metadata` object, or
|
||||
2. `params._meta["sandboxagent.dev"]` object.
|
||||
- Source:
|
||||
1. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:163`
|
||||
2. `server/packages/sandbox-agent/src/acp_runtime/ext_methods.rs:182`
|
||||
|
||||
### Keys with explicit runtime behavior
|
||||
- `title`:
|
||||
1. Updates `session.title` and stored sandbox metadata.
|
||||
- `model`:
|
||||
1. Updates model hint and stored sandbox metadata.
|
||||
- `mode`:
|
||||
1. Updates mode hint and stored sandbox metadata.
|
||||
- Source:
|
||||
1. `server/packages/sandbox-agent/src/acp_runtime/mod.rs:1355`
|
||||
2. `server/packages/sandbox-agent/src/acp_runtime/mod.rs:1369`
|
||||
3. `server/packages/sandbox-agent/src/acp_runtime/mod.rs:1374`
|
||||
4. `server/packages/sandbox-agent/src/acp_runtime/mod.rs:1377`
|
||||
|
||||
### Keys injected/derived by runtime in `session/list`
|
||||
- Runtime always injects these keys under `_meta["sandboxagent.dev"]`:
|
||||
1. `agent`
|
||||
2. `createdAt`
|
||||
3. `updatedAt`
|
||||
4. `ended`
|
||||
5. `eventCount`
|
||||
6. `model` (if model hint exists)
|
||||
- Source:
|
||||
1. `server/packages/sandbox-agent/src/acp_runtime/mod.rs:817`
|
||||
|
||||
### Known pass-through keys (stored and returned, not strongly typed in runtime)
|
||||
- Observed in tests/docs as pass-through metadata:
|
||||
1. `variant`
|
||||
2. `requestedSessionId`
|
||||
3. `permissionMode`
|
||||
4. `skills`
|
||||
5. `agentVersionRequested`
|
||||
- Sources:
|
||||
1. `server/packages/sandbox-agent/tests/v2_api/acp_extensions.rs:145`
|
||||
2. `research/acp/v1-schema-to-acp-mapping.md:73`
|
||||
3. `research/acp/v1-schema-to-acp-mapping.md:80`
|
||||
|
||||
## Package Split
|
||||
|
||||
### Package A: `acp-http-client`
|
||||
- Scope:
|
||||
1. ACP JSON-RPC over streamable HTTP only (`/v2/rpc`, headers, SSE replay, close).
|
||||
2. Generic envelope send/receive and connection lifecycle.
|
||||
3. No `_sandboxagent/*` helpers.
|
||||
4. No `_meta["sandboxagent.dev"]` helpers.
|
||||
5. No Sandbox-specific type aliases.
|
||||
- API intent:
|
||||
1. Low-level, minimal, protocol-faithful.
|
||||
2. Usable by any ACP-compatible server.
|
||||
|
||||
### Package B: `sandbox-agent` (`SandboxAgentClient`)
|
||||
- Scope:
|
||||
1. Control-plane and host APIs: health, agents, install, filesystem, etc.
|
||||
2. Single ACP-backed session client lifecycle hidden behind sandbox naming.
|
||||
3. Metadata conversion in/out of `_meta["sandboxagent.dev"]`.
|
||||
4. Sandbox extension conversion for `_sandboxagent/*` methods/events.
|
||||
- Lifecycle rules:
|
||||
1. Constructor: `new SandboxAgentClient(options)`.
|
||||
2. Auto-connect by default (configurable opt-out).
|
||||
3. `.connect(...)` creates/activates one ACP connection.
|
||||
4. `.connect(...)` throws if already connected.
|
||||
5. `.disconnect(...)` closes current ACP connection.
|
||||
6. ACP-related methods throw a not-connected error when disconnected.
|
||||
|
||||
## ACP-Shaped vs Sandbox API Names
|
||||
|
||||
ACP-shaped names are method names that mirror ACP primitives directly (or current SDK wrappers around them), such as `initialize`, `newSession`, `prompt`, `extMethod`.
|
||||
|
||||
Naming rule: for stable ACP methods, Sandbox Agent SDK method names stay ACP-aligned; only extension/unstable helpers may use Sandbox-specific naming.
|
||||
|
||||
| ACP-shaped name | ACP protocol message | Sandbox-facing name (candidate) | Notes |
|
||||
|---|---|---|---|
|
||||
| `initialize()` | `{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{...}}` | `connect()` | First request must include `params._meta[\"sandboxagent.dev\"].agent` when no connection id exists. |
|
||||
| `newSession()` | `{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"session/new\",\"params\":{...}}` | `newSession()` | Stable ACP method name preserved. |
|
||||
| `loadSession()` | `{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"session/load\",\"params\":{\"sessionId\":\"...\",...}}` | `loadSession()` | Stable ACP method name preserved. |
|
||||
| `prompt()` | `{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"session/prompt\",\"params\":{...}}` | `prompt()` | Stable ACP method name preserved. |
|
||||
| `cancel()` / `session/cancel` | `{\"jsonrpc\":\"2.0\",\"method\":\"session/cancel\",\"params\":{\"sessionId\":\"...\"}}` | `cancel()` | Stable ACP method name preserved. |
|
||||
| `setSessionMode()` | `{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"session/set_mode\",\"params\":{\"sessionId\":\"...\",\"modeId\":\"...\"}}` | `setSessionMode()` | Stable ACP method name preserved. |
|
||||
| `setSessionConfigOption()` | `{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"session/set_config_option\",\"params\":{...}}` | `setSessionConfigOption()` | Stable ACP method name preserved. |
|
||||
| `unstableListSessions()` or `session/list` | `{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"session/list\",\"params\":{...}}` | `listSessions()` | Wrapper chooses best server method. |
|
||||
| `unstableForkSession()` | `{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"session/fork\",\"params\":{...}}` | `forkSession()` | Preserve capability if exposed. |
|
||||
| `unstableResumeSession()` | `{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"session/resume\",\"params\":{...}}` | `resumeSession()` | Preserve capability if exposed. |
|
||||
| `unstableSetSessionModel()` / `session/set_model` | `{\"jsonrpc\":\"2.0\",\"id\":10,\"method\":\"session/set_model\",\"params\":{\"sessionId\":\"...\",\"modelId\":\"...\"}}` | `setSessionModel()` | ACP-aligned naming when exposed. |
|
||||
| `extMethod(\"_sandboxagent/session/list_models\")` | `{\"jsonrpc\":\"2.0\",\"id\":11,\"method\":\"_sandboxagent/session/list_models\",\"params\":{...}}` | `listModels()` | Native wrapper method. |
|
||||
| `extMethod(\"_sandboxagent/session/set_metadata\")` | `{\"jsonrpc\":\"2.0\",\"id\":12,\"method\":\"_sandboxagent/session/set_metadata\",\"params\":{...}}` | `setMetadata()` | Native wrapper method. |
|
||||
| `extMethod(\"_sandboxagent/session/detach\")` | `{\"jsonrpc\":\"2.0\",\"id\":13,\"method\":\"_sandboxagent/session/detach\",\"params\":{\"sessionId\":\"...\"}}` | `detachSession()` | Native wrapper method. |
|
||||
| `extMethod(\"_sandboxagent/session/terminate\")` | `{\"jsonrpc\":\"2.0\",\"id\":14,\"method\":\"_sandboxagent/session/terminate\",\"params\":{\"sessionId\":\"...\"}}` | `terminateSession()` | Native wrapper method. |
|
||||
| close ACP connection | `DELETE /v2/rpc` with header `x-acp-connection-id` | `disconnect()` | Transport-level close, not a JSON-RPC envelope. |
|
||||
|
||||
## Conversion Layer Requirements
|
||||
- Request conversion (sandbox -> ACP):
|
||||
1. Map sandbox method names to ACP methods.
|
||||
2. Inject/merge `_meta["sandboxagent.dev"]` where needed.
|
||||
- Response/event conversion (ACP -> sandbox):
|
||||
1. Convert `_sandboxagent/session/ended` to sandbox lifecycle event.
|
||||
2. Convert `_sandboxagent/agent/unparsed` to sandbox parse-error event.
|
||||
3. Surface metadata fields from `_meta["sandboxagent.dev"]` as first-class sandbox fields where appropriate.
|
||||
|
||||
## Error Model
|
||||
- Shared HTTP error type for non-2xx (`application/problem+json`) remains in sandbox SDK.
|
||||
- Additional wrapper errors:
|
||||
1. `NotConnectedError` for ACP-related calls before `.connect()`.
|
||||
2. `AlreadyConnectedError` when calling `.connect()` while connected.
|
||||
|
||||
## Rewrite Impact (expected)
|
||||
- Remove from `sandbox-agent` public API:
|
||||
1. `createAcpClient`
|
||||
2. `postAcpEnvelope`
|
||||
3. `closeAcpClient`
|
||||
4. ACP type re-exports from `@agentclientprotocol/sdk`
|
||||
5. ACP-named classes (`SandboxAgentAcpClient`)
|
||||
- Replace with sandbox-facing API on `SandboxAgentClient`.
|
||||
|
||||
## Testing Requirements
|
||||
- Continue integration tests against real server/runtime over real `/v2` HTTP APIs.
|
||||
- Add integration coverage for:
|
||||
1. Auto-connect on constructor.
|
||||
2. `autoConnect: false` behavior.
|
||||
3. Not-connected error gates.
|
||||
4. Single-connection guard (`connect()` twice).
|
||||
5. Metadata injection/extraction parity.
|
||||
6. Extension event conversion parity (`_sandboxagent/session/ended`, `_sandboxagent/agent/unparsed`).
|
||||
157
research/acp/v1-schema-to-acp-mapping.md
Normal file
157
research/acp/v1-schema-to-acp-mapping.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# V1 Schema to ACP v2 Mapping (1:1)
|
||||
|
||||
## 1) Scope
|
||||
|
||||
This document maps every current v1 HTTP endpoint and every universal event type to the v2 surface (ACP JSON-RPC for agent/session traffic, HTTP for control-plane/platform APIs).
|
||||
|
||||
Important: this is a conversion reference only. Runtime `/v1/*` endpoints are removed in v2 and return HTTP 410.
|
||||
|
||||
Source of truth used:
|
||||
|
||||
- Current API: `docs/openapi.json`
|
||||
- Current event schema: `docs/session-transcript-schema.mdx`
|
||||
- Event enums/types: `server/packages/universal-agent-schema/src/lib.rs`
|
||||
- ACP protocol docs: `~/misc/acp-docs/docs/protocol/*.mdx`
|
||||
- ACP schema/method map: `~/misc/acp-docs/schema/schema.json`, `~/misc/acp-docs/schema/meta.json`
|
||||
- ACP extensibility rules: `~/misc/acp-docs/docs/protocol/extensibility.mdx`
|
||||
|
||||
Transport assumption:
|
||||
|
||||
- v2 uses ACP over Streamable HTTP (POST + SSE) as the canonical public transport.
|
||||
- WebSocket can be added later without changing this mapping.
|
||||
|
||||
## 2) Mapping Rules
|
||||
|
||||
1. Use ACP standard methods/events first.
|
||||
2. If ACP stable has no equivalent, use either:
|
||||
- ACP extension methods (must start with `_`) for agent/session protocol gaps, or
|
||||
- HTTP `/v2/*` control-plane/platform endpoints for non-agent/session APIs.
|
||||
3. Preserve legacy-only data in `_meta` (namespaced), not as ad-hoc root fields.
|
||||
4. Use `_meta` for correlation and legacy envelope carry-over.
|
||||
|
||||
Extension namespace used in this spec:
|
||||
|
||||
- `_sandboxagent/...`
|
||||
|
||||
`_meta` namespace used in this spec:
|
||||
|
||||
- `_meta["sandboxagent.dev"]`
|
||||
|
||||
## 3) Endpoint Mapping (All v1 Endpoints)
|
||||
|
||||
| v1 endpoint | v2 mapping | Mapping type | Notes |
|
||||
|---|---|---|---|
|
||||
| `GET /v1/health` | `GET /v2/health` | HTTP control-plane | v1-parity payload on v2 route. |
|
||||
| `GET /v1/agents` | `GET /v2/agents` | HTTP control-plane | Agent inventory, capabilities, server status, and optional models/modes for installed agents. |
|
||||
| `POST /v1/agents/{agent}/install` | `POST /v2/agents/{agent}/install` | HTTP control-plane | Agent process + native agent install flow. |
|
||||
| `GET /v1/agents/{agent}/models` | Folded into agent response `models` field (installed agents only) | HTTP control-plane | No standalone `/models` endpoint in v2. |
|
||||
| `GET /v1/agents/{agent}/modes` | Folded into agent response `modes` field (installed agents only) | HTTP control-plane | No standalone `/modes` endpoint in v2. |
|
||||
| `GET /v1/fs/entries` | `GET /v2/fs/entries` | HTTP platform API | Port v1 behavior. |
|
||||
| `DELETE /v1/fs/entry` | `DELETE /v2/fs/entry` | HTTP platform API | Port v1 behavior, including `recursive`. |
|
||||
| `GET /v1/fs/file` | `GET /v2/fs/file` | HTTP platform API | Raw bytes response (octet-stream), v1 parity. |
|
||||
| `PUT /v1/fs/file` | `PUT /v2/fs/file` | HTTP platform API | Raw bytes write, v1 parity. |
|
||||
| `POST /v1/fs/mkdir` | `POST /v2/fs/mkdir` | HTTP platform API | Port v1 behavior. |
|
||||
| `POST /v1/fs/move` | `POST /v2/fs/move` | HTTP platform API | Port v1 behavior. |
|
||||
| `GET /v1/fs/stat` | `GET /v2/fs/stat` | HTTP platform API | Port v1 behavior. |
|
||||
| `POST /v1/fs/upload-batch` | `POST /v2/fs/upload-batch` | HTTP platform API | Tar upload/extract behavior from v1. |
|
||||
| `GET /v1/sessions` | `GET /v2/sessions` | HTTP control-plane | Session inventory without ACP connection requirement. |
|
||||
| `POST /v1/sessions/{session_id}` | `session/new` | Standard | Path `session_id` becomes alias in `_meta["sandboxagent.dev"].requestedSessionId`. |
|
||||
| `POST /v1/sessions/{session_id}/messages` | `session/prompt` | Standard | Asynchronous behavior comes from transport (request + stream). |
|
||||
| `POST /v1/sessions/{session_id}/messages/stream` | `session/prompt` + consume `session/update` on SSE | Standard | Streaming is transport-level, not a distinct ACP method. |
|
||||
| `POST /v1/sessions/{session_id}/terminate` | `_sandboxagent/session/terminate` | Extension | Idempotent termination semantics distinct from `DELETE /v2/rpc`. |
|
||||
| `GET /v1/sessions/{session_id}/events` | `_sandboxagent/session/events` (poll view over ACP stream) | Extension | Optional compatibility helper; canonical v2 is stream consumption. |
|
||||
| `GET /v1/sessions/{session_id}/events/sse` | `GET /v2/rpc` SSE stream | Standard transport | Filter by sessionId client-side or via connection/session binding. |
|
||||
| `POST /v1/sessions/{session_id}/permissions/{permission_id}/reply` | JSON-RPC response to pending `session/request_permission` request id | Standard | Bridge `permission_id` to request `id` in transport state. |
|
||||
| `POST /v1/sessions/{session_id}/questions/{question_id}/reply` | JSON-RPC response to pending `_sandboxagent/session/request_question` | Extension | ACP stable has no generic question/HITL request method. |
|
||||
| `POST /v1/sessions/{session_id}/questions/{question_id}/reject` | JSON-RPC response to pending `_sandboxagent/session/request_question` | Extension | Encode rejection in response outcome. |
|
||||
|
||||
### 3.1 `CreateSessionRequest` field mapping
|
||||
|
||||
| v1 field | ACP target | Notes |
|
||||
|---|---|---|
|
||||
| `agent` | `session/new.params._meta["sandboxagent.dev"].agent` (required) | Agent process selection is explicit session metadata, not an HTTP header. |
|
||||
| `agentVersion` | `_meta["sandboxagent.dev"].agentVersionRequested` | No ACP standard field. |
|
||||
| `directory` | `session/new.params.cwd` | Direct mapping. |
|
||||
| `mcp` | `session/new.params.mcpServers[]` | Convert map format to ACP array format. |
|
||||
| `agentMode` | `session/set_mode` or `session/set_config_option` | Prefer config option category `mode`; fallback to `_meta` hint. |
|
||||
| `model` | `session/set_config_option` | Prefer config option category `model`; fallback to `_meta` hint. |
|
||||
| `variant` | Deferred / out of scope | Do not implement in this pass. |
|
||||
| `permissionMode` | `session/set_config_option` or `_meta` | No ACP core permission mode field. |
|
||||
| `skills` | `_meta["sandboxagent.dev"].skills` | Product-specific. |
|
||||
| `title` | `_meta["sandboxagent.dev"].title` | Product-specific. |
|
||||
| path `session_id` | `_meta["sandboxagent.dev"].requestedSessionId` | Keep user-facing alias while ACP uses agent-generated `sessionId`. |
|
||||
|
||||
### 3.2 `MessageRequest` mapping
|
||||
|
||||
| v1 field | ACP target | Notes |
|
||||
|---|---|---|
|
||||
| `message` | `session/prompt.params.prompt[]` with `{"type":"text","text":...}` | Direct mapping. |
|
||||
| `attachments[].path` | `resource_link.uri` or embedded `resource.uri` | Use absolute `file://` URI. |
|
||||
| `attachments[].mime` | `mimeType` on ACP content block | Direct mapping when available. |
|
||||
| `attachments[].filename` | `_meta["sandboxagent.dev"].filename` on content block | Not a native ACP field. |
|
||||
|
||||
### 3.3 Permission/question reply mapping
|
||||
|
||||
| v1 request | ACP target | Notes |
|
||||
|---|---|---|
|
||||
| `PermissionReplyRequest.reply=once` | `session/request_permission` response outcome `selected` with option kind `allow_once` | Map by option kind first, then option id. |
|
||||
| `PermissionReplyRequest.reply=always` | outcome `selected` with option kind `allow_always` | If unavailable, fallback to closest allow option and record in `_meta`. |
|
||||
| `PermissionReplyRequest.reply=reject` | outcome `selected` with option kind `reject_once` or `reject_always` | Prefer exact semantic match from offered options. |
|
||||
| `QuestionReplyRequest.answers` | response to `_sandboxagent/session/request_question` | Preserve multi-select shape in `_meta["sandboxagent.dev"].answers`. |
|
||||
| reject question | response to `_sandboxagent/session/request_question` with `outcome="rejected"` | Extension response schema. |
|
||||
|
||||
## 4) Event Mapping (All Universal Event Types)
|
||||
|
||||
| Current event type | ACP message/event mapping | Mapping type | `_meta` carry-over |
|
||||
|---|---|---|---|
|
||||
| `session.started` | Derived from successful `session/new` (or `session/load`) response; optional notify `_sandboxagent/session/started` | Standard-derived + Extension optional | Carry `event_id`, `sequence`, `source`, `synthetic` in `_meta["sandboxagent.dev"].legacyEvent`. |
|
||||
| `session.ended` | `_sandboxagent/session/ended` notification | Extension | Include `{reason, terminated_by, message, exit_code}` under extension payload; preserve legacy envelope in `_meta`. |
|
||||
| `turn.started` | Derived when `session/prompt` request is accepted | Standard-derived | Preserve `turn_id` in `_meta["sandboxagent.dev"].turn`. |
|
||||
| `turn.ended` | `session/prompt` response (`stopReason`) | Standard | Map stop reason into legacy phase in `_meta`. |
|
||||
| `item.started` | `session/update` with `tool_call` for tool items; for message/reasoning items, first chunk plus `_meta.phase="started"` | Standard + `_meta` | Include `item_id`, `native_item_id`, `parent_id`, `kind`, `role`. |
|
||||
| `item.delta` | `session/update` with `agent_message_chunk`, `agent_thought_chunk`, or `tool_call_update.content` | Standard | Include legacy `item_id` in `_meta`. |
|
||||
| `item.completed` | `session/update` with `tool_call_update.status=completed/failed`; message completion inferred at turn end, optionally emit `_sandboxagent/item/completed` | Standard-derived + Extension optional | Include full finalized item snapshot in `_meta`. |
|
||||
| `error` | JSON-RPC error response when request-scoped; `_sandboxagent/error` for async/runtime errors | Standard + Extension | Preserve `code` and `details` in `_meta` if converted to JSON-RPC error object. |
|
||||
| `permission.requested` | `session/request_permission` request from agent | Standard | Store legacy `permission_id` to JSON-RPC request-id mapping in `_meta`. |
|
||||
| `permission.resolved` | JSON-RPC response to `session/request_permission` | Standard | Include selected option + mapped legacy status in `_meta`. |
|
||||
| `question.requested` | `_sandboxagent/session/request_question` request from agent | Extension | Keep `{question_id,prompt,options,status}` in params; preserve legacy envelope in `_meta`. |
|
||||
| `question.resolved` | JSON-RPC response to `_sandboxagent/session/request_question` | Extension | Include `response` and final status mapping in `_meta`. |
|
||||
| `agent.unparsed` | `_sandboxagent/agent/unparsed` notification | Extension | Carry `{error,location,raw_hash}`; include raw payload hash/correlation in `_meta`. |
|
||||
|
||||
## 5) `_meta` Contract for Legacy Parity
|
||||
|
||||
ACP extensibility rules require custom data in `_meta`. We reserve:
|
||||
|
||||
- Root keys for tracing: `traceparent`, `tracestate`, `baggage` (ACP recommendation)
|
||||
- Product namespace: `_meta["sandboxagent.dev"]`
|
||||
|
||||
Recommended shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"_meta": {
|
||||
"traceparent": "00-...",
|
||||
"sandboxagent.dev": {
|
||||
"connectionId": "acp_conn_123",
|
||||
"requestedSessionId": "my-session",
|
||||
"legacyEvent": {
|
||||
"eventId": "evt_123",
|
||||
"sequence": 42,
|
||||
"source": "agent",
|
||||
"synthetic": false,
|
||||
"nativeSessionId": "thread_abc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6) Known Gaps Requiring Extensions
|
||||
|
||||
These v1 capabilities are not covered by ACP stable methods and require `_sandboxagent/*` extensions:
|
||||
|
||||
- Session termination/history polling (`terminate`, `events` poll view)
|
||||
- Generic question/HITL request-reply flow
|
||||
- Session-ended notification payload parity (`_sandboxagent/session/ended`)
|
||||
|
||||
Track implementation friction and decisions in `research/acp/friction.md`.
|
||||
|
|
@ -9,6 +9,21 @@ Research notes on Claude Code's configuration, credential discovery, and runtime
|
|||
- **Session Persistence**: Session ID (string)
|
||||
- **SDK**: None (spawns CLI directly)
|
||||
|
||||
## ACP Terminology (Sandbox Agent v2)
|
||||
|
||||
Use these terms consistently when discussing Claude's ACP path:
|
||||
|
||||
| Term | Meaning |
|
||||
|------|---------|
|
||||
| **ACP agent process launcher** | The command used to start the ACP agent process, commonly an `npx` launcher script that executes `claude-code-acp`. |
|
||||
| **ACP agent process** | The running ACP agent process spawned by Sandbox Agent from the launcher command. |
|
||||
| **ACP client** | The client-visible transport handle identified by `X-ACP-Connection-Id`; requests and SSE are scoped to this ACP client. |
|
||||
|
||||
Related IDs:
|
||||
|
||||
- **ACP client ID**: value of `X-ACP-Connection-Id` (transport identity).
|
||||
- **ACP session ID**: `sessionId` returned by `session/new` (conversation/session identity within ACP).
|
||||
|
||||
## Credential Discovery
|
||||
|
||||
### Priority Order
|
||||
|
|
|
|||
|
|
@ -157,6 +157,41 @@ The server can send JSON-RPC requests (with `id`) for approvals:
|
|||
|
||||
These require JSON-RPC responses with a decision payload.
|
||||
|
||||
## App Server WebSocket Transport (Experimental)
|
||||
|
||||
Codex app-server also supports an experimental WebSocket transport:
|
||||
|
||||
```bash
|
||||
codex app-server --listen ws://127.0.0.1:4500
|
||||
```
|
||||
|
||||
### Transport constraints
|
||||
|
||||
- Listen URL must be `ws://IP:PORT` (not `localhost`, not `http://...`)
|
||||
- One JSON-RPC message per WebSocket text frame
|
||||
- Incoming: text frame JSON is parsed as a JSON-RPC message
|
||||
- Outgoing: JSON-RPC messages are serialized and sent as text frames
|
||||
- Ping/Pong is handled; binary frames are ignored
|
||||
|
||||
### Connection lifecycle
|
||||
|
||||
- Each accepted socket becomes a distinct connection with its own session state
|
||||
- Every connection must send `initialize` first
|
||||
- Sending non-`initialize` requests before init returns `"Not initialized"`
|
||||
- Sending `initialize` twice on the same connection returns `"Already initialized"`
|
||||
- Broadcast notifications are only sent to initialized connections
|
||||
|
||||
### Operational notes
|
||||
|
||||
- WebSocket mode is currently marked experimental/unsupported upstream
|
||||
- It is a raw WS server (no built-in TLS/auth); keep it on loopback or place it behind your own secure proxy/tunnel
|
||||
|
||||
### Upstream implementation references (openai/codex `main`, commit `03adb5db`)
|
||||
|
||||
- `codex-rs/app-server/src/transport.rs`
|
||||
- `codex-rs/app-server/src/message_processor.rs`
|
||||
- `codex-rs/app-server/README.md`
|
||||
|
||||
## Response Schema
|
||||
|
||||
```typescript
|
||||
|
|
|
|||
3
resources/agent-schemas/.gitignore
vendored
3
resources/agent-schemas/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
|||
node_modules/
|
||||
.cache/
|
||||
pnpm-lock.yaml
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://sandbox-agent/schemas/amp.json",
|
||||
"title": "AMP Code SDK Schema",
|
||||
"definitions": {
|
||||
"StreamJSONMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"system",
|
||||
"user",
|
||||
"assistant",
|
||||
"result",
|
||||
"message",
|
||||
"tool_call",
|
||||
"tool_result",
|
||||
"error",
|
||||
"done"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"tool_call": {
|
||||
"$ref": "#/definitions/ToolCall"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"subtype": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"mcp_servers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"type": "object"
|
||||
},
|
||||
"parent_tool_use_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"duration_ms": {
|
||||
"type": "number"
|
||||
},
|
||||
"is_error": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"num_turns": {
|
||||
"type": "number"
|
||||
},
|
||||
"result": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"AmpOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"baseURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxTokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"workingDirectory": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissionRules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionRule"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionRule": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tool": {
|
||||
"type": "string"
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny",
|
||||
"ask"
|
||||
]
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"tool",
|
||||
"action"
|
||||
]
|
||||
},
|
||||
"Message": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user",
|
||||
"assistant",
|
||||
"system"
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"tool_calls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ToolCall"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"content"
|
||||
]
|
||||
},
|
||||
"ToolCall": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"arguments": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"arguments"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://sandbox-agent/schemas/claude.json",
|
||||
"title": "Claude Code SDK Schema",
|
||||
"definitions": {
|
||||
"SDKMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user",
|
||||
"assistant",
|
||||
"result"
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"SDKResultMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "result"
|
||||
},
|
||||
"result": {
|
||||
"type": "object"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"duration_ms": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"Options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxTokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"systemPrompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"allowedTools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"workingDirectory": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"BashInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "number"
|
||||
},
|
||||
"workingDirectory": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command"
|
||||
]
|
||||
},
|
||||
"FileEditInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"oldText": {
|
||||
"type": "string"
|
||||
},
|
||||
"newText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"oldText",
|
||||
"newText"
|
||||
]
|
||||
},
|
||||
"FileReadInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"startLine": {
|
||||
"type": "number"
|
||||
},
|
||||
"endLine": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"FileWriteInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"content"
|
||||
]
|
||||
},
|
||||
"GlobInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
},
|
||||
"GrepInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"include": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
695
resources/agent-schemas/deno.lock
generated
695
resources/agent-schemas/deno.lock
generated
|
|
@ -1,695 +0,0 @@
|
|||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"npm:@anthropic-ai/claude-code@latest": "2.1.19",
|
||||
"npm:@openai/codex@latest": "0.91.0",
|
||||
"npm:@types/node@22": "22.19.7",
|
||||
"npm:cheerio@1": "1.2.0",
|
||||
"npm:ts-json-schema-generator@^2.4.0": "2.4.0",
|
||||
"npm:tsx@^4.19.0": "4.21.0",
|
||||
"npm:typescript@^5.7.0": "5.9.3"
|
||||
},
|
||||
"npm": {
|
||||
"@anthropic-ai/claude-code@2.1.19": {
|
||||
"integrity": "sha512-/bUlQuX/6nKr1Zqfi/9Q6xf7WonUBk72ZfKKENU4WVrIFWqTv/0JJsoW/dHol9QBNHvyfKIeBbYu4avHNRAnuQ==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-darwin-arm64",
|
||||
"@img/sharp-darwin-x64",
|
||||
"@img/sharp-linux-arm",
|
||||
"@img/sharp-linux-arm64",
|
||||
"@img/sharp-linux-x64",
|
||||
"@img/sharp-linuxmusl-arm64",
|
||||
"@img/sharp-linuxmusl-x64",
|
||||
"@img/sharp-win32-x64"
|
||||
],
|
||||
"bin": true
|
||||
},
|
||||
"@esbuild/aix-ppc64@0.27.2": {
|
||||
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
||||
"os": ["aix"],
|
||||
"cpu": ["ppc64"]
|
||||
},
|
||||
"@esbuild/android-arm64@0.27.2": {
|
||||
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
||||
"os": ["android"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/android-arm@0.27.2": {
|
||||
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
||||
"os": ["android"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@esbuild/android-x64@0.27.2": {
|
||||
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
||||
"os": ["android"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/darwin-arm64@0.27.2": {
|
||||
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/darwin-x64@0.27.2": {
|
||||
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/freebsd-arm64@0.27.2": {
|
||||
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
||||
"os": ["freebsd"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/freebsd-x64@0.27.2": {
|
||||
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
||||
"os": ["freebsd"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/linux-arm64@0.27.2": {
|
||||
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/linux-arm@0.27.2": {
|
||||
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@esbuild/linux-ia32@0.27.2": {
|
||||
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["ia32"]
|
||||
},
|
||||
"@esbuild/linux-loong64@0.27.2": {
|
||||
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["loong64"]
|
||||
},
|
||||
"@esbuild/linux-mips64el@0.27.2": {
|
||||
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["mips64el"]
|
||||
},
|
||||
"@esbuild/linux-ppc64@0.27.2": {
|
||||
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["ppc64"]
|
||||
},
|
||||
"@esbuild/linux-riscv64@0.27.2": {
|
||||
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["riscv64"]
|
||||
},
|
||||
"@esbuild/linux-s390x@0.27.2": {
|
||||
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["s390x"]
|
||||
},
|
||||
"@esbuild/linux-x64@0.27.2": {
|
||||
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/netbsd-arm64@0.27.2": {
|
||||
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
||||
"os": ["netbsd"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/netbsd-x64@0.27.2": {
|
||||
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
||||
"os": ["netbsd"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/openbsd-arm64@0.27.2": {
|
||||
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
||||
"os": ["openbsd"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/openbsd-x64@0.27.2": {
|
||||
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
||||
"os": ["openbsd"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/openharmony-arm64@0.27.2": {
|
||||
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
||||
"os": ["openharmony"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/sunos-x64@0.27.2": {
|
||||
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
||||
"os": ["sunos"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/win32-arm64@0.27.2": {
|
||||
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/win32-ia32@0.27.2": {
|
||||
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["ia32"]
|
||||
},
|
||||
"@esbuild/win32-x64@0.27.2": {
|
||||
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-darwin-arm64@0.33.5": {
|
||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-darwin-arm64"
|
||||
],
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-darwin-x64@0.33.5": {
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-darwin-x64"
|
||||
],
|
||||
"os": ["darwin"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-libvips-darwin-arm64@1.0.4": {
|
||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-libvips-darwin-x64@1.0.4": {
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-libvips-linux-arm64@1.0.4": {
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-libvips-linux-arm@1.0.5": {
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@img/sharp-libvips-linux-x64@1.0.4": {
|
||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-libvips-linuxmusl-arm64@1.0.4": {
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-libvips-linuxmusl-x64@1.0.4": {
|
||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-linux-arm64@0.33.5": {
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linux-arm64"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-linux-arm@0.33.5": {
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linux-arm"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@img/sharp-linux-x64@0.33.5": {
|
||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linux-x64"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-linuxmusl-arm64@0.33.5": {
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linuxmusl-arm64"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-linuxmusl-x64@0.33.5": {
|
||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linuxmusl-x64"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-win32-x64@0.33.5": {
|
||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@isaacs/balanced-match@4.0.1": {
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="
|
||||
},
|
||||
"@isaacs/brace-expansion@5.0.0": {
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dependencies": [
|
||||
"@isaacs/balanced-match"
|
||||
]
|
||||
},
|
||||
"@isaacs/cliui@8.0.2": {
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dependencies": [
|
||||
"string-width@5.1.2",
|
||||
"string-width-cjs@npm:string-width@4.2.3",
|
||||
"strip-ansi@7.1.2",
|
||||
"strip-ansi-cjs@npm:strip-ansi@6.0.1",
|
||||
"wrap-ansi@8.1.0",
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@7.0.0"
|
||||
]
|
||||
},
|
||||
"@openai/codex@0.91.0": {
|
||||
"integrity": "sha512-eRLRg0+uM0g0iW+Ca5VedBk+laslLcq93Hf6rbFtv+gLb4+aMib2UPdvlDlvvCVkBMbvE8ckY/cju+iOOuKCNA==",
|
||||
"bin": true
|
||||
},
|
||||
"@types/json-schema@7.0.15": {
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||
},
|
||||
"@types/node@22.19.7": {
|
||||
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
||||
"dependencies": [
|
||||
"undici-types"
|
||||
]
|
||||
},
|
||||
"ansi-regex@5.0.1": {
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||
},
|
||||
"ansi-regex@6.2.2": {
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="
|
||||
},
|
||||
"ansi-styles@4.3.0": {
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": [
|
||||
"color-convert"
|
||||
]
|
||||
},
|
||||
"ansi-styles@6.2.3": {
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="
|
||||
},
|
||||
"boolbase@1.0.0": {
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||
},
|
||||
"cheerio-select@2.1.0": {
|
||||
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||
"dependencies": [
|
||||
"boolbase",
|
||||
"css-select",
|
||||
"css-what",
|
||||
"domelementtype",
|
||||
"domhandler",
|
||||
"domutils"
|
||||
]
|
||||
},
|
||||
"cheerio@1.2.0": {
|
||||
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
|
||||
"dependencies": [
|
||||
"cheerio-select",
|
||||
"dom-serializer",
|
||||
"domhandler",
|
||||
"domutils",
|
||||
"encoding-sniffer",
|
||||
"htmlparser2",
|
||||
"parse5",
|
||||
"parse5-htmlparser2-tree-adapter",
|
||||
"parse5-parser-stream",
|
||||
"undici",
|
||||
"whatwg-mimetype"
|
||||
]
|
||||
},
|
||||
"color-convert@2.0.1": {
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": [
|
||||
"color-name"
|
||||
]
|
||||
},
|
||||
"color-name@1.1.4": {
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"commander@13.1.0": {
|
||||
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="
|
||||
},
|
||||
"cross-spawn@7.0.6": {
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dependencies": [
|
||||
"path-key",
|
||||
"shebang-command",
|
||||
"which"
|
||||
]
|
||||
},
|
||||
"css-select@5.2.2": {
|
||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||
"dependencies": [
|
||||
"boolbase",
|
||||
"css-what",
|
||||
"domhandler",
|
||||
"domutils",
|
||||
"nth-check"
|
||||
]
|
||||
},
|
||||
"css-what@6.2.2": {
|
||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="
|
||||
},
|
||||
"dom-serializer@2.0.0": {
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dependencies": [
|
||||
"domelementtype",
|
||||
"domhandler",
|
||||
"entities@4.5.0"
|
||||
]
|
||||
},
|
||||
"domelementtype@2.3.0": {
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
|
||||
},
|
||||
"domhandler@5.0.3": {
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dependencies": [
|
||||
"domelementtype"
|
||||
]
|
||||
},
|
||||
"domutils@3.2.2": {
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"dependencies": [
|
||||
"dom-serializer",
|
||||
"domelementtype",
|
||||
"domhandler"
|
||||
]
|
||||
},
|
||||
"eastasianwidth@0.2.0": {
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
},
|
||||
"emoji-regex@8.0.0": {
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"emoji-regex@9.2.2": {
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"encoding-sniffer@0.2.1": {
|
||||
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
|
||||
"dependencies": [
|
||||
"iconv-lite",
|
||||
"whatwg-encoding"
|
||||
]
|
||||
},
|
||||
"entities@4.5.0": {
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||
},
|
||||
"entities@6.0.1": {
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="
|
||||
},
|
||||
"entities@7.0.1": {
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="
|
||||
},
|
||||
"esbuild@0.27.2": {
|
||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||
"optionalDependencies": [
|
||||
"@esbuild/aix-ppc64",
|
||||
"@esbuild/android-arm",
|
||||
"@esbuild/android-arm64",
|
||||
"@esbuild/android-x64",
|
||||
"@esbuild/darwin-arm64",
|
||||
"@esbuild/darwin-x64",
|
||||
"@esbuild/freebsd-arm64",
|
||||
"@esbuild/freebsd-x64",
|
||||
"@esbuild/linux-arm",
|
||||
"@esbuild/linux-arm64",
|
||||
"@esbuild/linux-ia32",
|
||||
"@esbuild/linux-loong64",
|
||||
"@esbuild/linux-mips64el",
|
||||
"@esbuild/linux-ppc64",
|
||||
"@esbuild/linux-riscv64",
|
||||
"@esbuild/linux-s390x",
|
||||
"@esbuild/linux-x64",
|
||||
"@esbuild/netbsd-arm64",
|
||||
"@esbuild/netbsd-x64",
|
||||
"@esbuild/openbsd-arm64",
|
||||
"@esbuild/openbsd-x64",
|
||||
"@esbuild/openharmony-arm64",
|
||||
"@esbuild/sunos-x64",
|
||||
"@esbuild/win32-arm64",
|
||||
"@esbuild/win32-ia32",
|
||||
"@esbuild/win32-x64"
|
||||
],
|
||||
"scripts": true,
|
||||
"bin": true
|
||||
},
|
||||
"foreground-child@3.3.1": {
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dependencies": [
|
||||
"cross-spawn",
|
||||
"signal-exit"
|
||||
]
|
||||
},
|
||||
"fsevents@2.3.3": {
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"os": ["darwin"],
|
||||
"scripts": true
|
||||
},
|
||||
"get-tsconfig@4.13.0": {
|
||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||
"dependencies": [
|
||||
"resolve-pkg-maps"
|
||||
]
|
||||
},
|
||||
"glob@11.1.0": {
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"dependencies": [
|
||||
"foreground-child",
|
||||
"jackspeak",
|
||||
"minimatch",
|
||||
"minipass",
|
||||
"package-json-from-dist",
|
||||
"path-scurry"
|
||||
],
|
||||
"bin": true
|
||||
},
|
||||
"htmlparser2@10.1.0": {
|
||||
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||
"dependencies": [
|
||||
"domelementtype",
|
||||
"domhandler",
|
||||
"domutils",
|
||||
"entities@7.0.1"
|
||||
]
|
||||
},
|
||||
"iconv-lite@0.6.3": {
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dependencies": [
|
||||
"safer-buffer"
|
||||
]
|
||||
},
|
||||
"is-fullwidth-code-point@3.0.0": {
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"isexe@2.0.0": {
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"jackspeak@4.1.1": {
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"dependencies": [
|
||||
"@isaacs/cliui"
|
||||
]
|
||||
},
|
||||
"json5@2.2.3": {
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"bin": true
|
||||
},
|
||||
"lru-cache@11.2.5": {
|
||||
"integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="
|
||||
},
|
||||
"minimatch@10.1.1": {
|
||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||
"dependencies": [
|
||||
"@isaacs/brace-expansion"
|
||||
]
|
||||
},
|
||||
"minipass@7.1.2": {
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
|
||||
},
|
||||
"normalize-path@3.0.0": {
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||
},
|
||||
"nth-check@2.1.1": {
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"dependencies": [
|
||||
"boolbase"
|
||||
]
|
||||
},
|
||||
"package-json-from-dist@1.0.1": {
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"parse5-htmlparser2-tree-adapter@7.1.0": {
|
||||
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||
"dependencies": [
|
||||
"domhandler",
|
||||
"parse5"
|
||||
]
|
||||
},
|
||||
"parse5-parser-stream@7.1.2": {
|
||||
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||
"dependencies": [
|
||||
"parse5"
|
||||
]
|
||||
},
|
||||
"parse5@7.3.0": {
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"dependencies": [
|
||||
"entities@6.0.1"
|
||||
]
|
||||
},
|
||||
"path-key@3.1.1": {
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
|
||||
},
|
||||
"path-scurry@2.0.1": {
|
||||
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
|
||||
"dependencies": [
|
||||
"lru-cache",
|
||||
"minipass"
|
||||
]
|
||||
},
|
||||
"resolve-pkg-maps@1.0.0": {
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="
|
||||
},
|
||||
"safe-stable-stringify@2.5.0": {
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
|
||||
},
|
||||
"safer-buffer@2.1.2": {
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"shebang-command@2.0.0": {
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dependencies": [
|
||||
"shebang-regex"
|
||||
]
|
||||
},
|
||||
"shebang-regex@3.0.0": {
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||
},
|
||||
"signal-exit@4.1.0": {
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
|
||||
},
|
||||
"string-width@4.2.3": {
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": [
|
||||
"emoji-regex@8.0.0",
|
||||
"is-fullwidth-code-point",
|
||||
"strip-ansi@6.0.1"
|
||||
]
|
||||
},
|
||||
"string-width@5.1.2": {
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dependencies": [
|
||||
"eastasianwidth",
|
||||
"emoji-regex@9.2.2",
|
||||
"strip-ansi@7.1.2"
|
||||
]
|
||||
},
|
||||
"strip-ansi@6.0.1": {
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": [
|
||||
"ansi-regex@5.0.1"
|
||||
]
|
||||
},
|
||||
"strip-ansi@7.1.2": {
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"dependencies": [
|
||||
"ansi-regex@6.2.2"
|
||||
]
|
||||
},
|
||||
"ts-json-schema-generator@2.4.0": {
|
||||
"integrity": "sha512-HbmNsgs58CfdJq0gpteRTxPXG26zumezOs+SB9tgky6MpqiFgQwieCn2MW70+sxpHouZ/w9LW0V6L4ZQO4y1Ug==",
|
||||
"dependencies": [
|
||||
"@types/json-schema",
|
||||
"commander",
|
||||
"glob",
|
||||
"json5",
|
||||
"normalize-path",
|
||||
"safe-stable-stringify",
|
||||
"tslib",
|
||||
"typescript"
|
||||
],
|
||||
"bin": true
|
||||
},
|
||||
"tslib@2.8.1": {
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"tsx@4.21.0": {
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dependencies": [
|
||||
"esbuild",
|
||||
"get-tsconfig"
|
||||
],
|
||||
"optionalDependencies": [
|
||||
"fsevents"
|
||||
],
|
||||
"bin": true
|
||||
},
|
||||
"typescript@5.9.3": {
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"bin": true
|
||||
},
|
||||
"undici-types@6.21.0": {
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||
},
|
||||
"undici@7.19.1": {
|
||||
"integrity": "sha512-Gpq0iNm5M6cQWlyHQv9MV+uOj1jWk7LpkoE5vSp/7zjb4zMdAcUD+VL5y0nH4p9EbUklq00eVIIX/XcDHzu5xg=="
|
||||
},
|
||||
"whatwg-encoding@3.1.1": {
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"dependencies": [
|
||||
"iconv-lite"
|
||||
],
|
||||
"deprecated": true
|
||||
},
|
||||
"whatwg-mimetype@4.0.0": {
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="
|
||||
},
|
||||
"which@2.0.2": {
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dependencies": [
|
||||
"isexe"
|
||||
],
|
||||
"bin": true
|
||||
},
|
||||
"wrap-ansi@7.0.0": {
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dependencies": [
|
||||
"ansi-styles@4.3.0",
|
||||
"string-width@4.2.3",
|
||||
"strip-ansi@6.0.1"
|
||||
]
|
||||
},
|
||||
"wrap-ansi@8.1.0": {
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dependencies": [
|
||||
"ansi-styles@6.2.3",
|
||||
"string-width@5.1.2",
|
||||
"strip-ansi@7.1.2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@anthropic-ai/claude-code@latest",
|
||||
"npm:@openai/codex@latest",
|
||||
"npm:@types/node@22",
|
||||
"npm:cheerio@1",
|
||||
"npm:ts-json-schema-generator@^2.4.0",
|
||||
"npm:tsx@^4.19.0",
|
||||
"npm:typescript@^5.7.0"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"name": "agent-schemas",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"extract": "tsx src/index.ts",
|
||||
"extract:opencode": "tsx src/index.ts --agent=opencode",
|
||||
"extract:claude": "tsx src/index.ts --agent=claude",
|
||||
"extract:codex": "tsx src/index.ts --agent=codex",
|
||||
"extract:amp": "tsx src/index.ts --agent=amp",
|
||||
"extract:pi": "tsx src/index.ts --agent=pi",
|
||||
"extract:claude-events": "tsx src/claude-event-types.ts",
|
||||
"extract:claude-events:sdk": "tsx src/claude-event-types-sdk.ts",
|
||||
"extract:claude-events:cli": "tsx src/claude-event-types-cli.ts",
|
||||
"extract:claude-events:docs": "tsx src/claude-event-types-docs.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"ts-json-schema-generator": "^2.4.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"@anthropic-ai/claude-code": "latest",
|
||||
"@openai/codex": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.19.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/json-schema": "^7.0.15"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
import * as cheerio from "cheerio";
|
||||
import { fetchWithCache } from "./cache.js";
|
||||
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
|
||||
import type { JSONSchema7 } from "json-schema";
|
||||
|
||||
const AMP_DOCS_URL = "https://ampcode.com/manual/appendix?preview#message-schema";
|
||||
|
||||
// Key types we want to extract
|
||||
const TARGET_TYPES = ["StreamJSONMessage", "AmpOptions", "PermissionRule", "Message", "ToolCall"];
|
||||
|
||||
export async function extractAmpSchema(): Promise<NormalizedSchema> {
|
||||
console.log("Extracting AMP schema from documentation...");
|
||||
|
||||
try {
|
||||
const html = await fetchWithCache(AMP_DOCS_URL);
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// Find TypeScript code blocks
|
||||
const codeBlocks: string[] = [];
|
||||
$("pre code").each((_, el) => {
|
||||
const code = $(el).text();
|
||||
// Look for TypeScript interface/type definitions
|
||||
if (
|
||||
code.includes("interface ") ||
|
||||
code.includes("type ") ||
|
||||
code.includes(": {") ||
|
||||
code.includes("export ")
|
||||
) {
|
||||
codeBlocks.push(code);
|
||||
}
|
||||
});
|
||||
|
||||
if (codeBlocks.length === 0) {
|
||||
console.log(" [warn] No TypeScript code blocks found, using fallback schema");
|
||||
return createFallbackSchema();
|
||||
}
|
||||
|
||||
console.log(` [found] ${codeBlocks.length} code blocks`);
|
||||
|
||||
// Parse TypeScript definitions into schemas
|
||||
const definitions = parseTypeScriptToSchema(codeBlocks.join("\n"));
|
||||
|
||||
// Verify target types exist
|
||||
const found = TARGET_TYPES.filter((name) => definitions[name]);
|
||||
const missing = TARGET_TYPES.filter((name) => !definitions[name]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.log(` [warn] Missing expected types: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
if (Object.keys(definitions).length === 0) {
|
||||
console.log(" [warn] No types extracted, using fallback schema");
|
||||
return createFallbackSchema();
|
||||
}
|
||||
|
||||
console.log(` [ok] Extracted ${Object.keys(definitions).length} types (${found.length} target types)`);
|
||||
|
||||
return createNormalizedSchema("amp", "AMP Code SDK Schema", definitions);
|
||||
} catch (error) {
|
||||
console.log(` [error] Failed to fetch docs: ${error}`);
|
||||
console.log(" [fallback] Using embedded schema definitions");
|
||||
return createFallbackSchema();
|
||||
}
|
||||
}
|
||||
|
||||
function parseTypeScriptToSchema(code: string): Record<string, JSONSchema7> {
|
||||
const definitions: Record<string, JSONSchema7> = {};
|
||||
|
||||
// Match interface definitions
|
||||
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)\s*(?:extends\s+[\w,\s]+)?\s*\{([^}]+)\}/g;
|
||||
let match;
|
||||
|
||||
while ((match = interfaceRegex.exec(code)) !== null) {
|
||||
const [, name, body] = match;
|
||||
definitions[name] = parseInterfaceBody(body);
|
||||
}
|
||||
|
||||
// Match type definitions (simple object types)
|
||||
const typeRegex = /(?:export\s+)?type\s+(\w+)\s*=\s*\{([^}]+)\}/g;
|
||||
|
||||
while ((match = typeRegex.exec(code)) !== null) {
|
||||
const [, name, body] = match;
|
||||
definitions[name] = parseInterfaceBody(body);
|
||||
}
|
||||
|
||||
// Match union type definitions
|
||||
const unionRegex = /(?:export\s+)?type\s+(\w+)\s*=\s*([^;{]+);/g;
|
||||
|
||||
while ((match = unionRegex.exec(code)) !== null) {
|
||||
const [, name, body] = match;
|
||||
if (body.includes("|")) {
|
||||
definitions[name] = parseUnionType(body);
|
||||
}
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function parseInterfaceBody(body: string): JSONSchema7 {
|
||||
const properties: Record<string, JSONSchema7> = {};
|
||||
const required: string[] = [];
|
||||
|
||||
// Match property definitions
|
||||
const propRegex = /(\w+)(\?)?:\s*([^;]+);/g;
|
||||
let match;
|
||||
|
||||
while ((match = propRegex.exec(body)) !== null) {
|
||||
const [, propName, optional, propType] = match;
|
||||
properties[propName] = typeToSchema(propType.trim());
|
||||
|
||||
if (!optional) {
|
||||
required.push(propName);
|
||||
}
|
||||
}
|
||||
|
||||
const schema: JSONSchema7 = {
|
||||
type: "object",
|
||||
properties,
|
||||
};
|
||||
|
||||
if (required.length > 0) {
|
||||
schema.required = required;
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
function typeToSchema(tsType: string): JSONSchema7 {
|
||||
// Handle union types
|
||||
if (tsType.includes("|")) {
|
||||
return parseUnionType(tsType);
|
||||
}
|
||||
|
||||
// Handle array types
|
||||
if (tsType.endsWith("[]")) {
|
||||
const itemType = tsType.slice(0, -2);
|
||||
return {
|
||||
type: "array",
|
||||
items: typeToSchema(itemType),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle Array<T>
|
||||
const arrayMatch = tsType.match(/^Array<(.+)>$/);
|
||||
if (arrayMatch) {
|
||||
return {
|
||||
type: "array",
|
||||
items: typeToSchema(arrayMatch[1]),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle basic types
|
||||
switch (tsType) {
|
||||
case "string":
|
||||
return { type: "string" };
|
||||
case "number":
|
||||
return { type: "number" };
|
||||
case "boolean":
|
||||
return { type: "boolean" };
|
||||
case "null":
|
||||
return { type: "null" };
|
||||
case "any":
|
||||
case "unknown":
|
||||
return {};
|
||||
case "object":
|
||||
return { type: "object" };
|
||||
default:
|
||||
// Could be a reference to another type
|
||||
if (/^[A-Z]/.test(tsType)) {
|
||||
return { $ref: `#/definitions/${tsType}` };
|
||||
}
|
||||
// String literal
|
||||
if (tsType.startsWith('"') || tsType.startsWith("'")) {
|
||||
return { type: "string", const: tsType.slice(1, -1) };
|
||||
}
|
||||
return { type: "string" };
|
||||
}
|
||||
}
|
||||
|
||||
function parseUnionType(unionStr: string): JSONSchema7 {
|
||||
const parts = unionStr.split("|").map((p) => p.trim());
|
||||
|
||||
// Check if it's a string literal union
|
||||
const allStringLiterals = parts.every((p) => p.startsWith('"') || p.startsWith("'"));
|
||||
|
||||
if (allStringLiterals) {
|
||||
return {
|
||||
type: "string",
|
||||
enum: parts.map((p) => p.slice(1, -1)),
|
||||
};
|
||||
}
|
||||
|
||||
// General union
|
||||
return {
|
||||
oneOf: parts.map((p) => typeToSchema(p)),
|
||||
};
|
||||
}
|
||||
|
||||
function createFallbackSchema(): NormalizedSchema {
|
||||
// Fallback schema based on AMP documentation structure
|
||||
const definitions: Record<string, JSONSchema7> = {
|
||||
StreamJSONMessage: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: {
|
||||
type: "string",
|
||||
enum: ["system", "user", "assistant", "result", "message", "tool_call", "tool_result", "error", "done"],
|
||||
},
|
||||
// Common fields
|
||||
id: { type: "string" },
|
||||
content: { type: "string" },
|
||||
tool_call: { $ref: "#/definitions/ToolCall" },
|
||||
error: { type: "string" },
|
||||
// System message fields
|
||||
subtype: { type: "string" },
|
||||
cwd: { type: "string" },
|
||||
session_id: { type: "string" },
|
||||
tools: { type: "array", items: { type: "string" } },
|
||||
mcp_servers: { type: "array", items: { type: "object" } },
|
||||
// User/Assistant message fields
|
||||
message: { type: "object" },
|
||||
parent_tool_use_id: { type: "string" },
|
||||
// Result fields
|
||||
duration_ms: { type: "number" },
|
||||
is_error: { type: "boolean" },
|
||||
num_turns: { type: "number" },
|
||||
result: { type: "string" },
|
||||
},
|
||||
required: ["type"],
|
||||
},
|
||||
AmpOptions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
model: { type: "string" },
|
||||
apiKey: { type: "string" },
|
||||
baseURL: { type: "string" },
|
||||
maxTokens: { type: "number" },
|
||||
temperature: { type: "number" },
|
||||
systemPrompt: { type: "string" },
|
||||
tools: { type: "array", items: { type: "object" } },
|
||||
workingDirectory: { type: "string" },
|
||||
permissionRules: {
|
||||
type: "array",
|
||||
items: { $ref: "#/definitions/PermissionRule" },
|
||||
},
|
||||
},
|
||||
},
|
||||
PermissionRule: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tool: { type: "string" },
|
||||
action: { type: "string", enum: ["allow", "deny", "ask"] },
|
||||
pattern: { type: "string" },
|
||||
description: { type: "string" },
|
||||
},
|
||||
required: ["tool", "action"],
|
||||
},
|
||||
Message: {
|
||||
type: "object",
|
||||
properties: {
|
||||
role: { type: "string", enum: ["user", "assistant", "system"] },
|
||||
content: { type: "string" },
|
||||
tool_calls: {
|
||||
type: "array",
|
||||
items: { $ref: "#/definitions/ToolCall" },
|
||||
},
|
||||
},
|
||||
required: ["role", "content"],
|
||||
},
|
||||
ToolCall: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
name: { type: "string" },
|
||||
arguments: {
|
||||
oneOf: [{ type: "string" }, { type: "object" }],
|
||||
},
|
||||
},
|
||||
required: ["id", "name", "arguments"],
|
||||
},
|
||||
};
|
||||
|
||||
console.log(` [ok] Using fallback schema with ${Object.keys(definitions).length} definitions`);
|
||||
|
||||
return createNormalizedSchema("amp", "AMP Code SDK Schema", definitions);
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import { createHash } from "crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const CACHE_DIR = join(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"..",
|
||||
"resources",
|
||||
"agent-schemas",
|
||||
".cache"
|
||||
);
|
||||
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
function ensureCacheDir(): void {
|
||||
if (!existsSync(CACHE_DIR)) {
|
||||
mkdirSync(CACHE_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function hashKey(key: string): string {
|
||||
return createHash("sha256").update(key).digest("hex");
|
||||
}
|
||||
|
||||
function getCachePath(key: string): string {
|
||||
return join(CACHE_DIR, `${hashKey(key)}.json`);
|
||||
}
|
||||
|
||||
export function getCached<T>(key: string): T | null {
|
||||
const path = getCachePath(key);
|
||||
|
||||
if (!existsSync(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(path, "utf-8");
|
||||
const entry: CacheEntry<T> = JSON.parse(content);
|
||||
|
||||
const now = Date.now();
|
||||
if (now - entry.timestamp > entry.ttl) {
|
||||
// Cache expired
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setCache<T>(key: string, data: T, ttl: number = DEFAULT_TTL_MS): void {
|
||||
ensureCacheDir();
|
||||
|
||||
const entry: CacheEntry<T> = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
ttl,
|
||||
};
|
||||
|
||||
const path = getCachePath(key);
|
||||
writeFileSync(path, JSON.stringify(entry, null, 2));
|
||||
}
|
||||
|
||||
export async function fetchWithCache(url: string, ttl?: number): Promise<string> {
|
||||
const cached = getCached<string>(url);
|
||||
if (cached !== null) {
|
||||
console.log(` [cache hit] ${url}`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
console.log(` [fetching] ${url}`);
|
||||
|
||||
let lastError: Error | null = null;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
const text = await response.text();
|
||||
setCache(url, text, ttl);
|
||||
return text;
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempt < 2) {
|
||||
const delay = Math.pow(2, attempt) * 1000;
|
||||
console.log(` [retry ${attempt + 1}] waiting ${delay}ms...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { collectFromCli } from "./claude-event-types.js";
|
||||
|
||||
const promptArg = process.argv.slice(2).find((arg) => arg.startsWith("--prompt="));
|
||||
const timeoutArg = process.argv.slice(2).find((arg) => arg.startsWith("--timeoutMs="));
|
||||
|
||||
const prompt = promptArg?.split("=")[1] ?? "Reply with exactly OK.";
|
||||
const timeoutMs = timeoutArg ? Number(timeoutArg.split("=")[1]) : 20000;
|
||||
|
||||
collectFromCli(prompt, timeoutMs).then((result) => {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
});
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { collectFromDocs } from "./claude-event-types.js";
|
||||
|
||||
const urlsArg = process.argv.slice(2).find((arg) => arg.startsWith("--urls="));
|
||||
const urls = urlsArg ? urlsArg.split("=")[1]!.split(",") : undefined;
|
||||
|
||||
collectFromDocs(urls ?? []).then((result) => {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
});
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import { collectFromSdkTypes } from "./claude-event-types.js";
|
||||
|
||||
const result = collectFromSdkTypes();
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawn } from "node:child_process";
|
||||
import ts from "typescript";
|
||||
import { load } from "cheerio";
|
||||
|
||||
type SourceResult = {
|
||||
source: string;
|
||||
types: string[];
|
||||
details?: Record<string, string[]>;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const SDK_POSSIBLE_PATHS = [
|
||||
"node_modules/@anthropic-ai/claude-code/sdk-tools.d.ts",
|
||||
"node_modules/@anthropic-ai/claude-code/dist/index.d.ts",
|
||||
"node_modules/@anthropic-ai/claude-code/dist/types.d.ts",
|
||||
"node_modules/@anthropic-ai/claude-code/index.d.ts",
|
||||
];
|
||||
|
||||
const DEFAULT_DOC_URLS = [
|
||||
"https://platform.claude.com/docs/en/messages-streaming",
|
||||
"https://platform.claude.com/docs/en/api/messages-streaming",
|
||||
"https://docs.anthropic.com/claude/reference/messages-streaming",
|
||||
"https://docs.anthropic.com/claude/reference/messages-streaming#events",
|
||||
"https://docs.anthropic.com/claude/docs/messages-streaming",
|
||||
];
|
||||
|
||||
function moduleDir(): string {
|
||||
const metaDir = (import.meta as { dirname?: string }).dirname;
|
||||
if (typeof metaDir === "string") {
|
||||
return metaDir;
|
||||
}
|
||||
return dirname(fileURLToPath(import.meta.url));
|
||||
}
|
||||
|
||||
function findSdkTypesPath(): string | null {
|
||||
const resourceDir = join(moduleDir(), "..");
|
||||
const repoRoot = join(moduleDir(), "..", "..", "..");
|
||||
const searchRoots = [resourceDir, repoRoot];
|
||||
|
||||
for (const root of searchRoots) {
|
||||
for (const relativePath of SDK_POSSIBLE_PATHS) {
|
||||
const fullPath = join(root, relativePath);
|
||||
if (existsSync(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractStringLiterals(node: ts.TypeNode): string[] {
|
||||
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) {
|
||||
return [node.literal.text];
|
||||
}
|
||||
if (ts.isUnionTypeNode(node)) {
|
||||
return node.types.flatMap((typeNode) => extractStringLiterals(typeNode));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function containerName(node: ts.Node): string | null {
|
||||
let current: ts.Node | undefined = node;
|
||||
while (current) {
|
||||
if (ts.isInterfaceDeclaration(current) && current.name) {
|
||||
return current.name.text;
|
||||
}
|
||||
if (ts.isTypeAliasDeclaration(current) && current.name) {
|
||||
return current.name.text;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectFromSdkTypes(): SourceResult {
|
||||
const path = findSdkTypesPath();
|
||||
if (!path) {
|
||||
return { source: "sdk", types: [], error: "Claude SDK types not found" };
|
||||
}
|
||||
const content = readFileSync(path, "utf8");
|
||||
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
|
||||
const types = new Set<string>();
|
||||
const details: Record<string, string[]> = {};
|
||||
|
||||
function visit(node: ts.Node): void {
|
||||
if (ts.isPropertySignature(node)) {
|
||||
const name = node.name && ts.isIdentifier(node.name) ? node.name.text : null;
|
||||
if (name === "type" && node.type) {
|
||||
const literals = extractStringLiterals(node.type);
|
||||
if (literals.length > 0) {
|
||||
const parentName = containerName(node) ?? "anonymous";
|
||||
if (/Event|Stream|Message/i.test(parentName)) {
|
||||
literals.forEach((value) => types.add(value));
|
||||
details[parentName] = (details[parentName] ?? []).concat(literals);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return { source: "sdk", types: Array.from(types).sort(), details };
|
||||
}
|
||||
|
||||
function collectFromCli(prompt: string, timeoutMs: number): Promise<SourceResult> {
|
||||
return new Promise((resolve) => {
|
||||
const result: SourceResult = { source: "cli", types: [] };
|
||||
const types = new Set<string>();
|
||||
const denoGlobal = (globalThis as {
|
||||
Deno?: {
|
||||
which?: (cmd: string) => string | null;
|
||||
Command?: new (
|
||||
cmd: string,
|
||||
options: { args: string[]; stdout: "piped"; stderr: "piped" },
|
||||
) => { output: () => Promise<{ stdout: Uint8Array; stderr: Uint8Array; code: number }> };
|
||||
};
|
||||
}).Deno;
|
||||
|
||||
if (denoGlobal?.which && !denoGlobal.which("claude")) {
|
||||
result.error = "claude binary not found in PATH";
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
if (denoGlobal?.Command) {
|
||||
const command = new denoGlobal.Command("claude", {
|
||||
args: ["--print", "--output-format", "stream-json", "--verbose", prompt],
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
});
|
||||
try {
|
||||
command
|
||||
.output()
|
||||
.then(({ stdout, stderr, code }) => {
|
||||
const text = new TextDecoder().decode(stdout);
|
||||
for (const line of text.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const value = JSON.parse(trimmed);
|
||||
if (value && typeof value.type === "string") {
|
||||
types.add(value.type);
|
||||
}
|
||||
} catch {
|
||||
// ignore non-json
|
||||
}
|
||||
}
|
||||
result.types = Array.from(types).sort();
|
||||
if (code !== 0) {
|
||||
result.error =
|
||||
new TextDecoder().decode(stderr).trim() ||
|
||||
`claude exited with code ${code}`;
|
||||
}
|
||||
resolve(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
result.error = error instanceof Error ? error.message : String(error);
|
||||
resolve(result);
|
||||
});
|
||||
} catch (error) {
|
||||
result.error = error instanceof Error ? error.message : String(error);
|
||||
resolve(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let child;
|
||||
try {
|
||||
child = spawn(
|
||||
"claude",
|
||||
["--print", "--output-format", "stream-json", "--verbose", prompt],
|
||||
{ stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
} catch (error) {
|
||||
result.error = error instanceof Error ? error.message : String(error);
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!child.stdout || !child.stderr) {
|
||||
result.error = "claude stdout/stderr not available";
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
let stderr = "";
|
||||
const timer = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout.on("data", (chunk) => {
|
||||
const text = chunk.toString("utf8");
|
||||
for (const line of text.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const value = JSON.parse(trimmed);
|
||||
if (value && typeof value.type === "string") {
|
||||
types.add(value.type);
|
||||
}
|
||||
} catch {
|
||||
// ignore non-json
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString("utf8");
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timer);
|
||||
result.types = Array.from(types).sort();
|
||||
if (code !== 0) {
|
||||
result.error = stderr.trim() || `claude exited with code ${code}`;
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function collectFromDocs(urls: string[]): Promise<SourceResult> {
|
||||
if (typeof fetch !== "function") {
|
||||
return { source: "docs", types: [], error: "fetch is not available in this runtime" };
|
||||
}
|
||||
const effectiveUrls = urls.length > 0 ? urls : DEFAULT_DOC_URLS;
|
||||
const types = new Set<string>();
|
||||
const extractFromText = (text: string) => {
|
||||
const typeMatches = text.match(/\"type\"\\s*:\\s*\"([^\"]+)\"/g) ?? [];
|
||||
for (const match of typeMatches) {
|
||||
const value = match.split(":")[1]?.trim().replace(/^\"|\"$/g, "");
|
||||
if (value) types.add(value);
|
||||
}
|
||||
const eventMatches = text.match(/event\\s*:\\s*([a-z_]+)/gi) ?? [];
|
||||
for (const match of eventMatches) {
|
||||
const value = match.split(":")[1]?.trim();
|
||||
if (value) types.add(value);
|
||||
}
|
||||
};
|
||||
for (const url of effectiveUrls) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
continue;
|
||||
}
|
||||
const html = await res.text();
|
||||
const $ = load(html);
|
||||
const blocks = $("pre, code")
|
||||
.map((_, el) => $(el).text())
|
||||
.get();
|
||||
for (const block of blocks) {
|
||||
extractFromText(block);
|
||||
}
|
||||
const nextData = $("#__NEXT_DATA__").text();
|
||||
if (nextData) {
|
||||
extractFromText(nextData);
|
||||
}
|
||||
extractFromText(html);
|
||||
} catch {
|
||||
// ignore per-url errors
|
||||
}
|
||||
}
|
||||
return { source: "docs", types: Array.from(types).sort() };
|
||||
}
|
||||
|
||||
type Args = {
|
||||
source: "all" | "sdk" | "cli" | "docs";
|
||||
prompt: string;
|
||||
timeoutMs: number;
|
||||
urls: string[];
|
||||
json: boolean;
|
||||
};
|
||||
|
||||
function parseArgs(): Args {
|
||||
const args = process.argv.slice(2);
|
||||
const sourceArg = args.find((arg) => arg.startsWith("--source="));
|
||||
const promptArg = args.find((arg) => arg.startsWith("--prompt="));
|
||||
const timeoutArg = args.find((arg) => arg.startsWith("--timeoutMs="));
|
||||
const urlsArg = args.find((arg) => arg.startsWith("--urls="));
|
||||
const json = args.includes("--json");
|
||||
|
||||
return {
|
||||
source: (sourceArg?.split("=")[1] as Args["source"]) ?? "all",
|
||||
prompt: promptArg?.split("=")[1] ?? "Reply with exactly OK.",
|
||||
timeoutMs: timeoutArg ? Number(timeoutArg.split("=")[1]) : 20000,
|
||||
urls: urlsArg ? urlsArg.split("=")[1]!.split(",") : DEFAULT_DOC_URLS,
|
||||
json,
|
||||
};
|
||||
}
|
||||
|
||||
function summarize(results: SourceResult[]): void {
|
||||
const counts = results.map((r) => ({ source: r.source, count: r.types.length }));
|
||||
const max = Math.max(...counts.map((c) => c.count), 0);
|
||||
const best = counts.filter((c) => c.count === max).map((c) => c.source);
|
||||
const union = Array.from(
|
||||
new Set(results.flatMap((r) => r.types))
|
||||
).sort();
|
||||
|
||||
console.log("Claude event type extraction");
|
||||
console.log("============================");
|
||||
for (const result of results) {
|
||||
console.log(`- ${result.source}: ${result.types.length} types${result.error ? " (error)" : ""}`);
|
||||
}
|
||||
console.log(`\nMost comprehensive: ${best.join(", ") || "none"}`);
|
||||
console.log(`Union (${union.length}): ${union.join(", ")}`);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
const results: SourceResult[] = [];
|
||||
|
||||
if (args.source === "all" || args.source === "sdk") {
|
||||
results.push(collectFromSdkTypes());
|
||||
}
|
||||
if (args.source === "all" || args.source === "cli") {
|
||||
results.push(await collectFromCli(args.prompt, args.timeoutMs));
|
||||
}
|
||||
if (args.source === "all" || args.source === "docs") {
|
||||
results.push(await collectFromDocs(args.urls));
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
console.log(JSON.stringify({ results }, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
summarize(results);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
export { collectFromCli, collectFromDocs, collectFromSdkTypes };
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
import { execSync } from "child_process";
|
||||
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
|
||||
import type { JSONSchema7 } from "json-schema";
|
||||
|
||||
export async function extractClaudeSchema(): Promise<NormalizedSchema> {
|
||||
console.log("Extracting Claude Code schema via CLI...");
|
||||
|
||||
try {
|
||||
// Run claude CLI with --json-schema flag to get the schema
|
||||
const output = execSync("claude --output-format json --json-schema", {
|
||||
encoding: "utf-8",
|
||||
timeout: 30000,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
// Parse the JSON output
|
||||
const parsed = JSON.parse(output);
|
||||
|
||||
// Extract definitions from the schema
|
||||
const definitions: Record<string, JSONSchema7> = {};
|
||||
|
||||
if (parsed.definitions) {
|
||||
for (const [name, def] of Object.entries(parsed.definitions)) {
|
||||
definitions[name] = def as JSONSchema7;
|
||||
}
|
||||
} else if (parsed.$defs) {
|
||||
for (const [name, def] of Object.entries(parsed.$defs)) {
|
||||
definitions[name] = def as JSONSchema7;
|
||||
}
|
||||
} else {
|
||||
// The output might be a single schema, use it as the root
|
||||
definitions["Schema"] = parsed as JSONSchema7;
|
||||
}
|
||||
|
||||
console.log(` [ok] Extracted ${Object.keys(definitions).length} types from CLI`);
|
||||
|
||||
return createNormalizedSchema("claude", "Claude Code SDK Schema", definitions);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.log(` [warn] CLI extraction failed: ${errorMessage}`);
|
||||
console.log(" [fallback] Using embedded schema definitions");
|
||||
return createFallbackSchema();
|
||||
}
|
||||
}
|
||||
|
||||
function createFallbackSchema(): NormalizedSchema {
|
||||
// Fallback schema based on known SDK structure
|
||||
const definitions: Record<string, JSONSchema7> = {
|
||||
SDKMessage: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: { type: "string", enum: ["user", "assistant", "result"] },
|
||||
content: { type: "string" },
|
||||
timestamp: { type: "string", format: "date-time" },
|
||||
},
|
||||
required: ["type"],
|
||||
},
|
||||
SDKResultMessage: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: { type: "string", const: "result" },
|
||||
result: { type: "object" },
|
||||
error: { type: "string" },
|
||||
duration_ms: { type: "number" },
|
||||
},
|
||||
required: ["type"],
|
||||
},
|
||||
Options: {
|
||||
type: "object",
|
||||
properties: {
|
||||
model: { type: "string" },
|
||||
maxTokens: { type: "number" },
|
||||
temperature: { type: "number" },
|
||||
systemPrompt: { type: "string" },
|
||||
tools: { type: "array", items: { type: "string" } },
|
||||
allowedTools: { type: "array", items: { type: "string" } },
|
||||
workingDirectory: { type: "string" },
|
||||
},
|
||||
},
|
||||
BashInput: {
|
||||
type: "object",
|
||||
properties: {
|
||||
command: { type: "string" },
|
||||
timeout: { type: "number" },
|
||||
workingDirectory: { type: "string" },
|
||||
},
|
||||
required: ["command"],
|
||||
},
|
||||
FileEditInput: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
oldText: { type: "string" },
|
||||
newText: { type: "string" },
|
||||
},
|
||||
required: ["path", "oldText", "newText"],
|
||||
},
|
||||
FileReadInput: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
startLine: { type: "number" },
|
||||
endLine: { type: "number" },
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
FileWriteInput: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
content: { type: "string" },
|
||||
},
|
||||
required: ["path", "content"],
|
||||
},
|
||||
GlobInput: {
|
||||
type: "object",
|
||||
properties: {
|
||||
pattern: { type: "string" },
|
||||
path: { type: "string" },
|
||||
},
|
||||
required: ["pattern"],
|
||||
},
|
||||
GrepInput: {
|
||||
type: "object",
|
||||
properties: {
|
||||
pattern: { type: "string" },
|
||||
path: { type: "string" },
|
||||
include: { type: "string" },
|
||||
},
|
||||
required: ["pattern"],
|
||||
},
|
||||
};
|
||||
|
||||
console.log(` [ok] Using fallback schema with ${Object.keys(definitions).length} definitions`);
|
||||
|
||||
return createNormalizedSchema("claude", "Claude Code SDK Schema", definitions);
|
||||
}
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
import { execSync } from "child_process";
|
||||
import { existsSync, readFileSync, rmSync, readdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
|
||||
import type { JSONSchema7 } from "json-schema";
|
||||
|
||||
function normalizeCodexRefs(value: JSONSchema7): JSONSchema7 {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => normalizeCodexRefs(item as JSONSchema7)) as JSONSchema7;
|
||||
}
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
const next: Record<string, JSONSchema7> = {};
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if (key === "$ref" && typeof child === "string") {
|
||||
next[key] = child.replace("#/definitions/v2/", "#/definitions/") as JSONSchema7;
|
||||
continue;
|
||||
}
|
||||
next[key] = normalizeCodexRefs(child as JSONSchema7);
|
||||
}
|
||||
return next as JSONSchema7;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function extractCodexSchema(): Promise<NormalizedSchema> {
|
||||
console.log("Extracting Codex schema via CLI...");
|
||||
|
||||
const tempDir = join(import.meta.dirname, "..", ".temp-codex-schemas");
|
||||
|
||||
try {
|
||||
// Run codex CLI to generate JSON schema
|
||||
execSync(`codex app-server generate-json-schema --out "${tempDir}"`, {
|
||||
encoding: "utf-8",
|
||||
timeout: 30000,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
// Read generated schema files from temp directory
|
||||
const definitions: Record<string, JSONSchema7> = {};
|
||||
|
||||
if (existsSync(tempDir)) {
|
||||
const files = readdirSync(tempDir).filter((f) => f.endsWith(".json"));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(tempDir, file);
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const schema = JSON.parse(content);
|
||||
|
||||
// Extract the name from the file (e.g., "ThreadEvent.json" -> "ThreadEvent")
|
||||
const name = file.replace(".json", "");
|
||||
|
||||
if (schema.definitions) {
|
||||
for (const [defName, def] of Object.entries(schema.definitions)) {
|
||||
definitions[defName] = normalizeCodexRefs(def as JSONSchema7);
|
||||
}
|
||||
} else if (schema.$defs) {
|
||||
for (const [defName, def] of Object.entries(schema.$defs)) {
|
||||
definitions[defName] = normalizeCodexRefs(def as JSONSchema7);
|
||||
}
|
||||
} else {
|
||||
definitions[name] = normalizeCodexRefs(schema as JSONSchema7);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up temp directory
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (Object.keys(definitions).length === 0) {
|
||||
console.log(" [warn] No schemas extracted from CLI, using fallback");
|
||||
return createFallbackSchema();
|
||||
}
|
||||
|
||||
console.log(` [ok] Extracted ${Object.keys(definitions).length} types from CLI`);
|
||||
|
||||
return createNormalizedSchema("codex", "Codex SDK Schema", definitions);
|
||||
} catch (error) {
|
||||
// Clean up temp directory on error
|
||||
if (existsSync(tempDir)) {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.log(` [warn] CLI extraction failed: ${errorMessage}`);
|
||||
console.log(" [fallback] Using embedded schema definitions");
|
||||
return createFallbackSchema();
|
||||
}
|
||||
}
|
||||
|
||||
function createFallbackSchema(): NormalizedSchema {
|
||||
// Fallback schema based on known SDK structure
|
||||
const definitions: Record<string, JSONSchema7> = {
|
||||
ThreadEvent: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: {
|
||||
type: "string",
|
||||
enum: ["thread.created", "thread.updated", "item.created", "item.updated", "error"],
|
||||
},
|
||||
thread_id: { type: "string" },
|
||||
item: { $ref: "#/definitions/ThreadItem" },
|
||||
error: { type: "object" },
|
||||
},
|
||||
required: ["type"],
|
||||
},
|
||||
ThreadItem: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
type: { type: "string", enum: ["message", "function_call", "function_result"] },
|
||||
role: { type: "string", enum: ["user", "assistant", "system"] },
|
||||
content: {
|
||||
oneOf: [{ type: "string" }, { type: "array", items: { type: "object" } }],
|
||||
},
|
||||
status: { type: "string", enum: ["pending", "in_progress", "completed", "failed"] },
|
||||
},
|
||||
required: ["id", "type"],
|
||||
},
|
||||
CodexOptions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
apiKey: { type: "string" },
|
||||
model: { type: "string" },
|
||||
baseURL: { type: "string" },
|
||||
maxTokens: { type: "number" },
|
||||
temperature: { type: "number" },
|
||||
},
|
||||
},
|
||||
ThreadOptions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
instructions: { type: "string" },
|
||||
tools: { type: "array", items: { type: "object" } },
|
||||
model: { type: "string" },
|
||||
workingDirectory: { type: "string" },
|
||||
},
|
||||
},
|
||||
Input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: { type: "string", enum: ["text", "file", "image"] },
|
||||
content: { type: "string" },
|
||||
path: { type: "string" },
|
||||
mimeType: { type: "string" },
|
||||
},
|
||||
required: ["type"],
|
||||
},
|
||||
ResponseItem: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: { type: "string" },
|
||||
id: { type: "string" },
|
||||
content: { type: "string" },
|
||||
function_call: { $ref: "#/definitions/FunctionCall" },
|
||||
},
|
||||
},
|
||||
FunctionCall: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
arguments: { type: "string" },
|
||||
call_id: { type: "string" },
|
||||
},
|
||||
required: ["name", "arguments"],
|
||||
},
|
||||
Message: {
|
||||
type: "object",
|
||||
properties: {
|
||||
role: { type: "string", enum: ["user", "assistant", "system"] },
|
||||
content: { type: "string" },
|
||||
},
|
||||
required: ["role", "content"],
|
||||
},
|
||||
};
|
||||
|
||||
console.log(` [ok] Using fallback schema with ${Object.keys(definitions).length} definitions`);
|
||||
|
||||
return createNormalizedSchema("codex", "Codex SDK Schema", definitions);
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
import { writeFileSync, existsSync, mkdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { extractOpenCodeSchema } from "./opencode.js";
|
||||
import { extractClaudeSchema } from "./claude.js";
|
||||
import { extractCodexSchema } from "./codex.js";
|
||||
import { extractAmpSchema } from "./amp.js";
|
||||
import { extractPiSchema } from "./pi.js";
|
||||
import { validateSchema, type NormalizedSchema } from "./normalize.js";
|
||||
|
||||
const RESOURCE_DIR = join(import.meta.dirname, "..");
|
||||
const DIST_DIR = join(RESOURCE_DIR, "artifacts", "json-schema");
|
||||
|
||||
type AgentName = "opencode" | "claude" | "codex" | "amp" | "pi";
|
||||
|
||||
const EXTRACTORS: Record<AgentName, () => Promise<NormalizedSchema>> = {
|
||||
opencode: extractOpenCodeSchema,
|
||||
claude: extractClaudeSchema,
|
||||
codex: extractCodexSchema,
|
||||
amp: extractAmpSchema,
|
||||
pi: extractPiSchema,
|
||||
};
|
||||
|
||||
function parseArgs(): { agents: AgentName[] } {
|
||||
const args = process.argv.slice(2);
|
||||
const agentArg = args.find((arg) => arg.startsWith("--agent="));
|
||||
|
||||
if (agentArg) {
|
||||
const agent = agentArg.split("=")[1] as AgentName;
|
||||
if (!EXTRACTORS[agent]) {
|
||||
console.error(`Unknown agent: ${agent}`);
|
||||
console.error(`Valid agents: ${Object.keys(EXTRACTORS).join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return { agents: [agent] };
|
||||
}
|
||||
|
||||
return { agents: Object.keys(EXTRACTORS) as AgentName[] };
|
||||
}
|
||||
|
||||
function ensureDistDir(): void {
|
||||
if (!existsSync(DIST_DIR)) {
|
||||
mkdirSync(DIST_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function extractAndWrite(agent: AgentName): Promise<boolean> {
|
||||
try {
|
||||
const extractor = EXTRACTORS[agent];
|
||||
const schema = await extractor();
|
||||
|
||||
// Validate schema
|
||||
const validation = validateSchema(schema);
|
||||
if (!validation.valid) {
|
||||
console.error(` [error] Schema validation failed for ${agent}:`);
|
||||
validation.errors.forEach((err) => console.error(` - ${err}`));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write to file
|
||||
const outputPath = join(DIST_DIR, `${agent}.json`);
|
||||
writeFileSync(outputPath, JSON.stringify(schema, null, 2));
|
||||
console.log(` [wrote] ${outputPath}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(` [error] Failed to extract ${agent}: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("Agent Schema Extractor");
|
||||
console.log("======================\n");
|
||||
|
||||
const { agents } = parseArgs();
|
||||
ensureDistDir();
|
||||
|
||||
console.log(`Extracting schemas for: ${agents.join(", ")}\n`);
|
||||
|
||||
const results: Record<string, boolean> = {};
|
||||
|
||||
for (const agent of agents) {
|
||||
results[agent] = await extractAndWrite(agent);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log("Summary");
|
||||
console.log("-------");
|
||||
|
||||
const successful = Object.entries(results)
|
||||
.filter(([, success]) => success)
|
||||
.map(([name]) => name);
|
||||
const failed = Object.entries(results)
|
||||
.filter(([, success]) => !success)
|
||||
.map(([name]) => name);
|
||||
|
||||
if (successful.length > 0) {
|
||||
console.log(`Successful: ${successful.join(", ")}`);
|
||||
}
|
||||
if (failed.length > 0) {
|
||||
console.log(`Failed: ${failed.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("\nDone!");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import type { JSONSchema7 } from "json-schema";
|
||||
|
||||
export interface NormalizedSchema {
|
||||
$schema: string;
|
||||
$id: string;
|
||||
title: string;
|
||||
definitions: Record<string, JSONSchema7>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts OpenAPI 3.1 schema to JSON Schema draft-07.
|
||||
* OpenAPI 3.1 is largely compatible with JSON Schema draft 2020-12,
|
||||
* but we want draft-07 for broader tool compatibility.
|
||||
*/
|
||||
export function openApiToJsonSchema(schema: Record<string, unknown>): JSONSchema7 {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
// Skip OpenAPI-specific fields
|
||||
if (key === "discriminator" || key === "xml" || key === "externalDocs") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle nullable (OpenAPI 3.0 style)
|
||||
if (key === "nullable" && value === true) {
|
||||
continue; // Will be handled by type conversion
|
||||
}
|
||||
|
||||
// Recursively convert nested schemas
|
||||
if (key === "properties" && typeof value === "object" && value !== null) {
|
||||
result[key] = {};
|
||||
for (const [propName, propSchema] of Object.entries(value as Record<string, unknown>)) {
|
||||
(result[key] as Record<string, unknown>)[propName] = openApiToJsonSchema(
|
||||
propSchema as Record<string, unknown>
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "items" && typeof value === "object" && value !== null) {
|
||||
result[key] = openApiToJsonSchema(value as Record<string, unknown>);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "additionalProperties" && typeof value === "object" && value !== null) {
|
||||
result[key] = openApiToJsonSchema(value as Record<string, unknown>);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((key === "oneOf" || key === "anyOf" || key === "allOf") && Array.isArray(value)) {
|
||||
result[key] = value.map((item) =>
|
||||
typeof item === "object" && item !== null
|
||||
? openApiToJsonSchema(item as Record<string, unknown>)
|
||||
: item
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert $ref paths from OpenAPI to local definitions
|
||||
if (key === "$ref" && typeof value === "string") {
|
||||
result[key] = value.replace("#/components/schemas/", "#/definitions/");
|
||||
continue;
|
||||
}
|
||||
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
// Handle nullable by adding null to type array
|
||||
if (schema["nullable"] === true && result["type"]) {
|
||||
const currentType = result["type"];
|
||||
if (Array.isArray(currentType)) {
|
||||
if (!currentType.includes("null")) {
|
||||
result["type"] = [...currentType, "null"];
|
||||
}
|
||||
} else {
|
||||
result["type"] = [currentType as string, "null"];
|
||||
}
|
||||
}
|
||||
|
||||
return result as JSONSchema7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a normalized schema with consistent metadata.
|
||||
*/
|
||||
export function createNormalizedSchema(
|
||||
id: string,
|
||||
title: string,
|
||||
definitions: Record<string, JSONSchema7>
|
||||
): NormalizedSchema {
|
||||
return {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
$id: `https://sandbox-agent/schemas/${id}.json`,
|
||||
title,
|
||||
definitions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a schema against JSON Schema draft-07 meta-schema.
|
||||
* Basic validation - checks required fields and structure.
|
||||
*/
|
||||
export function validateSchema(schema: unknown): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (typeof schema !== "object" || schema === null) {
|
||||
return { valid: false, errors: ["Schema must be an object"] };
|
||||
}
|
||||
|
||||
const s = schema as Record<string, unknown>;
|
||||
|
||||
if (s.$schema && typeof s.$schema !== "string") {
|
||||
errors.push("$schema must be a string");
|
||||
}
|
||||
|
||||
if (s.definitions && typeof s.definitions !== "object") {
|
||||
errors.push("definitions must be an object");
|
||||
}
|
||||
|
||||
if (s.definitions && typeof s.definitions === "object") {
|
||||
for (const [name, def] of Object.entries(s.definitions as Record<string, unknown>)) {
|
||||
if (typeof def !== "object" || def === null) {
|
||||
errors.push(`Definition "${name}" must be an object`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { fetchWithCache } from "./cache.js";
|
||||
import { createNormalizedSchema, openApiToJsonSchema, type NormalizedSchema } from "./normalize.js";
|
||||
import type { JSONSchema7 } from "json-schema";
|
||||
|
||||
const OPENAPI_URLS = [
|
||||
"https://raw.githubusercontent.com/anomalyco/opencode/dev/packages/sdk/openapi.json",
|
||||
"https://raw.githubusercontent.com/sst/opencode/dev/packages/sdk/openapi.json",
|
||||
];
|
||||
|
||||
// Key schemas we want to extract
|
||||
const TARGET_SCHEMAS = [
|
||||
"Session",
|
||||
"Message",
|
||||
"Part",
|
||||
"Event",
|
||||
"PermissionRequest",
|
||||
"QuestionRequest",
|
||||
"TextPart",
|
||||
"ToolCallPart",
|
||||
"ToolResultPart",
|
||||
"ErrorPart",
|
||||
];
|
||||
|
||||
const OPENAPI_ARTIFACT_DIR = join(import.meta.dirname, "..", "artifacts", "openapi");
|
||||
const OPENAPI_ARTIFACT_PATH = join(OPENAPI_ARTIFACT_DIR, "opencode.json");
|
||||
|
||||
interface OpenAPISpec {
|
||||
components?: {
|
||||
schemas?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
function writeOpenApiArtifact(specText: string): void {
|
||||
if (!existsSync(OPENAPI_ARTIFACT_DIR)) {
|
||||
mkdirSync(OPENAPI_ARTIFACT_DIR, { recursive: true });
|
||||
}
|
||||
writeFileSync(OPENAPI_ARTIFACT_PATH, specText);
|
||||
console.log(` [wrote] ${OPENAPI_ARTIFACT_PATH}`);
|
||||
}
|
||||
|
||||
export async function extractOpenCodeSchema(): Promise<NormalizedSchema> {
|
||||
console.log("Extracting OpenCode schema from OpenAPI spec...");
|
||||
|
||||
let specText: string | null = null;
|
||||
let lastError: Error | null = null;
|
||||
for (const url of OPENAPI_URLS) {
|
||||
try {
|
||||
specText = await fetchWithCache(url);
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
}
|
||||
}
|
||||
if (!specText) {
|
||||
throw lastError ?? new Error("Failed to fetch OpenCode OpenAPI spec");
|
||||
}
|
||||
writeOpenApiArtifact(specText);
|
||||
const spec: OpenAPISpec = JSON.parse(specText);
|
||||
|
||||
if (!spec.components?.schemas) {
|
||||
throw new Error("OpenAPI spec missing components.schemas");
|
||||
}
|
||||
|
||||
const definitions: Record<string, JSONSchema7> = {};
|
||||
|
||||
// Extract all schemas, not just target ones, to preserve references
|
||||
for (const [name, schema] of Object.entries(spec.components.schemas)) {
|
||||
definitions[name] = openApiToJsonSchema(schema as Record<string, unknown>);
|
||||
}
|
||||
|
||||
// Verify target schemas exist
|
||||
const missing = TARGET_SCHEMAS.filter((name) => !definitions[name]);
|
||||
if (missing.length > 0) {
|
||||
console.warn(` [warn] Missing expected schemas: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
const found = TARGET_SCHEMAS.filter((name) => definitions[name]);
|
||||
console.log(` [ok] Extracted ${Object.keys(definitions).length} schemas (${found.length} target schemas)`);
|
||||
|
||||
return createNormalizedSchema("opencode", "OpenCode SDK Schema", definitions);
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
37
sdks/acp-http-client/package.json
Normal file
37
sdks/acp-http-client/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "acp-http-client",
|
||||
"version": "0.1.0",
|
||||
"description": "Protocol-faithful ACP JSON-RPC over streamable HTTP client.",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rivet-dev/sandbox-agent"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.14.1"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue