diff --git a/specs/v2/api.html b/specs/v2/api.html new file mode 100644 index 0000000000..c23d7d4f00 --- /dev/null +++ b/specs/v2/api.html @@ -0,0 +1,781 @@ + + + + + + opencode v2 API + + + +
+
+
+
opencode v2
+

API map

+
+
+

+ A single /api route surface for simple clients and multi-directory frontends. The important + design question is not route nesting; it is where runtime context comes from. +

+
+ Server scoped + Request context + Session pinned +
+
+
+ +
+
+ Everything has one canonical route. Some routes are server-scoped; runtime routes use context; session item routes use the session. +

+ Server-scoped routes manage the whole server: projects, workspace lifecycle, and auth accounts. Runtime + context is for anything resolved from an active directory, including config, provider capabilities, tools, + files, and VCS. +

+
+ +
+ +
+
+

Context Model

+ + API context resolution + Non-session routes resolve from request context, session item routes resolve from session storage. + + + + + + + + Non-session route + /api/file, /api/vcs/status + + + Request context + query params or default runtime + + + Runtime context + directory + workspaceID? + + + Session item route + /api/session/:id/prompt + + + Session row + contains pinned context + + + Runtime context + directory + workspaceID? + +
+ +
+

Request-context calls

+

+ These calls operate against a directory, optionally through a workspace. Simple clients omit context and + use the default runtime. +

+
GET /api/fs/tree?path=.&directory=/repo/app&workspace=ws_123
+
+ +
+

Session-pinned calls

+

+ These calls never take request context. The session is already pinned to the directory and workspace it was + created in. +

+
POST /api/session/ses_123/prompt
+
+// server resolves
+sessionID -> { directory, workspaceID? }
+
+
+ +
+
+

Operation Inventory

+

+ The SDK is the source of truth. HTTP routes are mounts for RPC-style operations. server operations do not use runtime context. request operations use request/default runtime context from directory and workspace query parameters. session operations use pinned session context and should not accept context input. +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OperationInputContextHTTP mountPurpose
agent.list{}requestGET /api/agentAvailable agents.
auth.activate{ accountID: AccountID }serverPOST /api/auth/:accountID/activateSet the account as active for its service.
auth.create{ + serviceID: ServiceID + credential: + | { type: "oauth", refresh: string, access: string, expires: number } + | { type: "api", key: string, metadata?: Record<string, string> } + description?: string + active?: boolean +}serverPOST /api/authCreate an auth account.
auth.delete{ accountID: AccountID }serverDELETE /api/auth/:accountIDRemove an auth account.
auth.get{ accountID: AccountID }serverGET /api/auth/:accountIDGet one auth account.
auth.list{ serviceID?: ServiceID }serverGET /api/authList saved auth accounts. Response includes active account mapping.
auth.update{ + accountID: AccountID + description?: string + credential?: + | { type: "oauth", refresh: string, access: string, expires: number } + | { type: "api", key: string, metadata?: Record<string, string> } +}serverPATCH /api/auth/:accountIDUpdate account description or credential.
catalog.model.get{ + providerID: ProviderID + modelID: ModelID +}serverGET /api/catalog/model/:providerID/:modelIDGet one catalog model.
catalog.model.list{}serverGET /api/catalog/modelList flattened catalog models.
command.list{}requestGET /api/commandAvailable commands.
config.get{}requestGET /api/configResolved config.
config.update{ config: Config }requestPATCH /api/configUpdate config.
event.subscribe{}requestGET /api/eventServer-sent events for the resolved runtime context.
formatter.status{}requestGET /api/formatterFormatter status.
fs.file{ path: string }requestGET /api/fs/fileRead one file.
fs.grep{ + pattern: string + include?: string + limit?: number +}requestPOST /api/fs/grepSearch file contents.
fs.search{ + query: string + type?: "file" | "directory" + limit?: number +}requestPOST /api/fs/searchSearch paths by name.
fs.tree{ path: string }requestGET /api/fs/treeBrowse a directory.
lsp.status{}requestGET /api/lspLSP status.
mcp.prompt.list{}requestGET /api/mcp/promptList MCP prompts.
mcp.prompt.render{ + server: string + name: string + arguments?: Record<string, string> +}requestPOST /api/mcp/prompt/renderRender one MCP prompt.
mcp.resource.list{}requestGET /api/mcp/resourceList MCP resources.
mcp.resource.read{ + server: string + uri: string +}requestGET /api/mcp/resource/readRead one MCP resource.
mcp.server.create{ + name: string + config: + | { type: "local", command: string, arguments?: string[], environment?: Record<string, string> } + | { type: "remote", url: string, headers?: Record<string, string>, oauth?: boolean | object } +}requestPOST /api/mcp/serverAdd an MCP server to runtime config.
mcp.server.list{}requestGET /api/mcp/serverList MCP servers with status and auth state.
mcp.server.oauth.callback{ + name: string + code: string +}requestPOST /api/mcp/server/:name/oauth/callbackComplete MCP OAuth.
mcp.server.oauth.delete{ name: string }requestDELETE /api/mcp/server/:name/oauthRemove MCP OAuth credentials.
mcp.server.oauth.start{ name: string }requestPOST /api/mcp/server/:name/oauthStart MCP OAuth.
permission.list{}requestGET /api/permissionPending permission requests.
permission.reply{ + permissionID: PermissionID + response: PermissionReply +}requestPOST /api/permission/:permissionID/replyReply to a permission request.
project.get{ projectID: ProjectID }serverGET /api/project/:projectIDGet project metadata.
project.list{}serverGET /api/projectList projects known to this server.
project.update{ + projectID: ProjectID + name?: string + icon?: string + commands?: Array<{ + name: string + command: string + }> +}serverPATCH /api/project/:projectIDUpdate project metadata.
provider.list{}requestGET /api/providerProvider inventory for the runtime context.
pty.create{ + command?: string + cwd?: string + shell?: string +}requestPOST /api/ptyCreate PTY in the runtime context.
pty.delete{ ptyID: PtyID }requestDELETE /api/pty/:ptyIDDelete PTY.
pty.get{ ptyID: PtyID }requestGET /api/pty/:ptyIDGet PTY info.
pty.list{}requestGET /api/ptyList PTYs for the runtime.
pty.update{ + ptyID: PtyID + title?: string + size?: { columns: number, rows: number } +}requestPATCH /api/pty/:ptyIDUpdate PTY.
question.list{}requestGET /api/questionPending user questions.
question.reject{ questionID: QuestionID }requestPOST /api/question/:questionID/rejectReject a question.
question.reply{ + questionID: QuestionID + response: QuestionResponse +}requestPOST /api/question/:questionID/replyReply to a question.
session.compact{ sessionID: SessionID }sessionPOST /api/session/:sessionID/compactCompact the session conversation.
session.context{ sessionID: SessionID }sessionGET /api/session/:sessionID/contextReturn active context messages after the last compaction.
session.create{ + title?: string + agent?: string + model?: { providerID: ProviderID, modelID: ModelID } + permission?: PermissionRule[] +}requestPOST /api/sessionCreate a session pinned to resolved runtime context.
session.delete{ sessionID: SessionID }sessionDELETE /api/session/:sessionIDDelete a session.
session.diff{ sessionID: SessionID }sessionGET /api/session/:sessionID/diffReturn session diff summary.
session.get{ sessionID: SessionID }sessionGET /api/session/:sessionIDGet one session.
session.list{ + limit?: number + order?: "asc" | "desc" + path?: string + roots?: boolean + start?: number + search?: string + cursor?: string +}requestGET /api/sessionList sessions for the current runtime context by default.
session.message.list{ + sessionID: SessionID + limit?: number + order?: "asc" | "desc" + cursor?: string +}sessionGET /api/session/:sessionID/messagePage through session messages.
session.prompt{ + sessionID: SessionID + prompt: Prompt + delivery?: "immediate" | "deferred" +}sessionPOST /api/session/:sessionID/promptCreate a user message and queue the agent loop.
session.todo{ sessionID: SessionID }sessionGET /api/session/:sessionID/todoReturn todos associated with the session.
session.update{ + sessionID: SessionID + title?: string + archived?: number + permission?: PermissionRule[] +}sessionPATCH /api/session/:sessionIDUpdate title, archival state, or session metadata.
session.wait{ sessionID: SessionID }sessionPOST /api/session/:sessionID/waitWait until the session is idle.
skill.list{}requestGET /api/skillAvailable skills.
vcs.diff{ + format?: "json" | "patch" + mode?: "worktree" | "default" +}requestGET /api/vcs/diffDiff for the runtime directory.
vcs.get{}requestGET /api/vcsVCS metadata.
vcs.patch{ patch: string }requestPOST /api/vcs/patchApply a patch to the runtime directory.
vcs.status{}requestGET /api/vcs/statusChanged files.
workspace.create{ + projectID?: ProjectID + name?: string + directory?: string + type: string + metadata?: Record<string, unknown> +}serverPOST /api/workspaceCreate or register a workspace.
workspace.delete{ workspaceID: WorkspaceID }serverDELETE /api/workspace/:workspaceIDRemove a workspace registration.
workspace.get{ workspaceID: WorkspaceID }serverGET /api/workspace/:workspaceIDGet workspace metadata.
workspace.list{ projectID?: ProjectID }serverGET /api/workspaceList workspaces, optionally filtered by project.
workspace.status{}serverGET /api/workspace/statusConnection/lifecycle status for all workspaces. Needs team discussion.
workspace.sync{}serverPOST /api/workspace/syncSync workspace metadata from adapters. Needs team discussion.
workspace.update{ + workspaceID: WorkspaceID + name?: string + metadata?: Record<string, unknown> + archived?: boolean +}serverPATCH /api/workspace/:workspaceIDUpdate workspace metadata or lifecycle state.
workspace.warp{ + workspaceID?: WorkspaceID + sessionID: SessionID + copyChanges: boolean +}serverPOST /api/workspace/warpMove a session into or out of a workspace. Needs team discussion.
+
+
+ +
+
+

Event Envelope

+

+ Every event uses the same envelope. Resource identity belongs in payload. Runtime identity belongs + in context. +

+
+
type ApiEvent<Payload> = {
+  id: string
+  type: string
+  time: number
+  context: {
+    directory: string
+    workspaceID?: string
+  }
+  payload: Payload
+}
+
{
+  "id": "evt_01",
+  "type": "message.part.delta",
+  "time": 1760000000000,
+  "context": {
+    "directory": "/repo/app",
+    "workspaceID": "ws_123"
+  },
+  "payload": {
+    "sessionID": "ses_123",
+    "messageID": "msg_456",
+    "partID": "part_789",
+    "field": "text",
+    "delta": "hello"
+  }
+}
+
+
+
+ +
+
+

Frontend Sync Store

+

+ A frontend can keep one giant store like the current TUI. Runtime data is partitioned by + contextKey. Durable entities such as sessions and messages are keyed by their own IDs. +

+
type RuntimeContext = {
+  directory: string
+  workspaceID?: string
+}
+
+type ContextKey = string
+type SessionID = string
+type MessageID = string
+
+type SyncStore = {
+  status: "loading" | "partial" | "complete"
+
+  shared: {
+    provider: Provider[]
+    provider_default: Record<string, string>
+    provider_next: ProviderListResponse
+    provider_auth: Record<string, ProviderAuthMethod[]>
+    console_state: ConsoleState
+  }
+
+  contexts: Record<
+    ContextKey,
+    {
+      context: RuntimeContext
+
+      config: Config
+      agent: Agent[]
+      command: Command[]
+      lsp: LspStatus[]
+      formatter: FormatterStatus[]
+      vcs: VcsInfo | undefined
+      mcp: Record<string, McpStatus>
+      mcp_resource: Record<string, McpResource>
+
+      session: SessionID[]
+      session_status: Record<SessionID, SessionStatus>
+    }
+  >
+
+  session: Record<SessionID, Session & { context: RuntimeContext }>
+  session_diff: Record<SessionID, Snapshot.FileDiff[]>
+  todo: Record<SessionID, Todo[]>
+  permission: Record<SessionID, PermissionRequest[]>
+  question: Record<SessionID, QuestionRequest[]>
+
+  message: Record<SessionID, Message[]>
+  part: Record<MessageID, Part[]>
+}
+
+function contextKey(context: RuntimeContext) {
+  return `${context.workspaceID ?? "local"}:${context.directory}`
+}
+
+
+
+ +