mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-20 07:13:33 +00:00
docs(v2): consolidate specifications (#36186)
This commit is contained in:
parent
8f04a09d1a
commit
d54038b9d2
15 changed files with 206 additions and 2571 deletions
|
|
@ -156,9 +156,9 @@ const table = sqliteTable("session", {
|
|||
- Keep durable events minimal: record irreducible new facts and do not repeat state derivable by folding the ordered aggregate history. Enrich projections and read models with previous or derived state when consumers need self-contained views.
|
||||
- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_pending` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries, consuming the pending row in the same event transaction; `session_pending` stores only unconsumed work.
|
||||
- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Retry of an already-promoted input reconciles against the projected message and the durable admitted event rather than a retained row.
|
||||
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
|
||||
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; interruption of a known but idle or locally unowned Session is a no-op, while the public API rejects an unknown Session.
|
||||
- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
|
||||
- Preserve one explicit `llm.stream(request)` call per step and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop.
|
||||
- Preserve one explicit `llm.stream(request)` call per Physical Attempt and reload projected history before durable continuation. Most Steps have one Physical Attempt; overflow-triggered compaction recovery may rebuild one Step for a second attempt. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop.
|
||||
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
|
||||
- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe step boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's step allowance; a batch of steers resets it once.
|
||||
- One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle.
|
||||
|
|
|
|||
10
CONTEXT.md
10
CONTEXT.md
|
|
@ -182,12 +182,12 @@ _Avoid_: Response envelope
|
|||
- SDK executes Server's assembled `HttpRouter` in memory. It opens no listener and performs no network I/O, while preserving Server routing, middleware, codecs, handlers, and errors.
|
||||
- The Effect Client and SDK re-export their decoded datatype facade from Schema so callers do not depend on internal package locations or Core's versioned names.
|
||||
- A capability intended for both networked and **Embedded OpenCode** belongs in the authoritative public `HttpApi`; embedded-only same-process capabilities extend **Embedded OpenCode** separately.
|
||||
- `sessions.events({ sessionID, after })` is a public durable Session event stream. It verifies the Session, replays durable events after the optional aggregate sequence, continues with newly committed durable events, excludes live-only fragments, and is transported as SSE in both networked and embedded modes.
|
||||
- `sessions.log({ sessionID, after, follow })` is the public durable Session event stream. It verifies the Session, replays durable events after the optional aggregate sequence, optionally continues with newly committed durable events, excludes live-only fragments, and is transported as SSE in both networked and embedded modes.
|
||||
- `events.subscribe()` is a distinct public instance-wide live stream for Session and non-Session activity. It has no replay guarantee and includes connection, heartbeat, and instance-disposal lifecycle events; consumers recover from disconnection by refreshing authoritative state.
|
||||
- A Session ID is not an optional filter on `events.subscribe()`: instance-wide live events and durable Session events have different schemas, replay guarantees, cursors, lifecycle events, and failure behavior.
|
||||
- The initial common OpenCode Client does not expose server-global event aggregation. `events.subscribe()` is bounded to the connected OpenCode instance or workspace; any future cross-instance administrative stream requires a separately designed API.
|
||||
- `events.subscribe()` does not automatically reconnect after transport loss. The live-only stream fails with `ClientError`; consumers refresh authoritative state before explicitly opening a new subscription because events missed during disconnection cannot be replayed.
|
||||
- `sessions.events({ sessionID, after })` returns the generated HTTP client's cold durable event stream and does not build reconnection policy into the endpoint or client constructor. Transport loss fails the stream with `ClientError`. Callers may compose an explicit resuming stream above it by retaining the last observed durable sequence and opening a new subscription with `after`; any reusable resume helper remains a separate API design question.
|
||||
- `sessions.log({ sessionID, after, follow })` returns the generated HTTP client's cold durable event stream and does not build reconnection policy into the endpoint or client constructor. Transport loss fails the stream with `ClientError`. Callers may compose an explicit resuming stream above it by retaining the last observed durable sequence and opening a new subscription with `after`; any reusable resume helper remains a separate API design question.
|
||||
- The stable `sessions.list(...)` design returns a **Page** in both networked and **Embedded OpenCode**; embedded execution does not define a separate unbounded array-returning list operation. The beta client currently preserves the existing HTTP `{ data, cursor }` envelope until emitter-level Page projection is implemented.
|
||||
- Session list cursors are opaque branded values carrying continuation query and ordering state. Consumers pass them back unchanged and do not inspect storage anchors or encoded filter fields.
|
||||
- A Session list continuation accepts only its opaque cursor. Scope, filters, ordering, and page size are fixed by the initial query and carried by that cursor.
|
||||
|
|
@ -209,13 +209,13 @@ _Avoid_: Response envelope
|
|||
- Oversized textual **Model Tool Output** retains a bounded preview in Session history while its complete text moves to managed tool-output storage. Arbitrary structured-result size is a separate concern.
|
||||
- One tool settlement receives one aggregate textual limit, using the configured maximum lines or UTF-8 bytes, whichever is reached first. The limit is provider-independent; token pressure belongs to context assembly and compaction.
|
||||
- Generic truncation preserves the beginning and end of textual output. Tools may apply a more meaningful strategy before the Tool Registry enforces the final limit.
|
||||
- A truncated **Model Tool Output** identifies its complete text both in the bounded model-visible preview and as a typed managed output path. Managed output paths do not modify the tool's validated structured result.
|
||||
- A truncated **Model Tool Output** identifies its complete text in the bounded model-visible preview. The Tool Registry also supplies managed paths as internal metadata to tool hooks; Session events do not expose a typed `outputPaths` field.
|
||||
- A **Managed Tool Output File** is temporary and may expire after its retention period. The bounded **Model Tool Output**, not the file, is the durable replayable record.
|
||||
- Failure to retain a **Managed Tool Output File** does not change a successful tool operation into a failed one. The Session records an explicitly lossy bounded output without a path, while operators receive diagnostics for the storage failure.
|
||||
- Failure to retain a **Managed Tool Output File** fails settlement operationally. The Session never publishes a successful result whose complete output was lost during generic bounding.
|
||||
- Once a tool operation succeeds, bounding its **Model Tool Output** and publishing its one durable settlement form an interruption-safe completion region. Raw oversized success is never published before a later correction.
|
||||
- When a structured-only result would exceed the **Model Tool Output** limit, its validated structured value remains unchanged for Session consumers while model replay uses a bounded textual JSON preview and optional managed output path.
|
||||
- Existing tool-managed output paths survive generic bounding. A fallback file retains exactly the complete projected text received by the Tool Registry and never claims to reconstruct output already discarded by tool-specific shaping.
|
||||
- **Managed Tool Output Files** use globally unique names in one shared flat directory. Their absolute paths are readable and searchable by ordinary tools; other absolute paths remain outside Location-scoped filesystem authority.
|
||||
- **Managed Tool Output Files** use globally unique names in one shared flat directory. They receive no special filesystem authority; each tool applies its ordinary external-path policy.
|
||||
- Provider-executed tool results remain provider-native transcript facts outside generic Tool Registry bounding. Their context control requires provider-aware pruning or compaction because some providers require exact structured round-trip payloads.
|
||||
|
||||
## Client contract architecture
|
||||
|
|
|
|||
|
|
@ -6,54 +6,53 @@ This file tracks the gap between the native `@opencode-ai/llm` package and the A
|
|||
|
||||
## Existing Status Sources
|
||||
|
||||
| File | What it tracks | Limitation |
|
||||
| --- | --- | --- |
|
||||
| `packages/llm/DESIGN.md` | Future clean-break API proposal, currently named `@opencode-ai/ai` in the draft. | Not a provider parity tracker. |
|
||||
| `packages/llm/example/call-sites.md` | Route/value/provider-facade migration checklist and call-site sketches. | Architecture migration only; not AI SDK package parity. |
|
||||
| `specs/v2/provider-model.md` | V2 catalog endpoint schema and current Session runner adaptation surface. | Runner-specific; not a native LLM package status matrix. |
|
||||
| File | What it tracks | Limitation |
|
||||
| ------------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| `packages/llm/DESIGN.md` | Future clean-break API proposal, currently named `@opencode-ai/ai` in the draft. | Not a provider parity tracker. |
|
||||
| `packages/llm/example/call-sites.md` | Route/value/provider-facade migration checklist and call-site sketches. | Architecture migration only; not AI SDK package parity. |
|
||||
|
||||
## Current Implementation Snapshot
|
||||
|
||||
| Native slice | Source | Current state | Main gaps |
|
||||
| --- | --- | --- | --- |
|
||||
| OpenAI Chat | `src/protocols/openai-chat.ts`, `src/providers/openai.ts` | Usable. Streams text, reasoning deltas, tool calls, usage, images, and common generation controls. | No typed structured-output / `response_format` path. Limited typed OpenAI option surface compared with SDK escape hatches. |
|
||||
| OpenAI Responses HTTP | `src/protocols/openai-responses.ts`, `src/providers/openai.ts` | Usable. Supports hosted-tool event surfacing, reasoning replay metadata, GPT-5 defaults, and cache usage. | No explicit `previous_response_id` path. Typed options cover only a subset of Responses fields. Structured output is still mostly synthetic-tool based. |
|
||||
| OpenAI Responses WebSocket | `src/protocols/openai-responses.ts`, `src/route/transport/websocket.ts` | Present as `OpenAI.responsesWebSocket(...)`. | Runner/catalog support explicitly must not downgrade WebSocket routes; broader runtime selection is not complete. |
|
||||
| OpenAI-compatible Chat | `src/protocols/openai-compatible-chat.ts`, `src/providers/openai-compatible.ts` | Usable for generic Chat and several profiles: Baseten, Cerebras, DeepInfra, DeepSeek, Fireworks, Groq, TogetherAI. | No OpenAI-compatible Responses protocol/facade. Family quirks are mostly endpoint defaults, not full typed behavior. |
|
||||
| Anthropic Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic.ts` | Usable. Supports tools, thinking, cache control, images, server-hosted tool events, and usage. | Provider option surface is small. Beta/header handling, metadata, and newer Messages fields need a typed parity pass. |
|
||||
| Gemini Developer API | `src/protocols/gemini.ts`, `src/providers/google.ts` | Usable for Google API key flow. Supports text, images, tools, thinking signatures, and cache usage. | This is not Vertex. Typed provider options are narrow; many Gemini request fields currently require raw `http.body` overlays. |
|
||||
| Bedrock Converse | `src/protocols/bedrock-converse.ts`, `src/providers/amazon-bedrock.ts` | Partial but real. Supports AWS event-stream framing, SigV4 with supplied credentials, bearer auth, tools, reasoning signatures, media, cache points, and recorded tests. | Native facade does not mirror the AI SDK plugin's default AWS credential chain/profile behavior. Runner/catalog mapping is missing. Guardrails, inference profiles, region-specific model ID fixes, and model-specific request fields need a parity pass. |
|
||||
| Azure OpenAI | `src/providers/azure.ts` using OpenAI Chat/Responses protocols | Partial. Supports resource/base URL setup, API key auth, API version query, Chat, and Responses selectors. | Core runner does not map `@ai-sdk/azure` to this native facade. AAD/token auth and Azure-specific endpoint variants need review. |
|
||||
| Cloudflare AI Gateway / Workers AI | `src/providers/cloudflare.ts` | Present via OpenAI-compatible Chat routes. | Useful but not part of the critical AI SDK replacement set yet. Needs per-product recorded coverage before relying on it broadly. |
|
||||
| OpenRouter | `src/providers/openrouter.ts` | Present with OpenRouter-specific usage/reasoning/prompt-cache options over Chat. | Responses-style OpenRouter support is absent. |
|
||||
| xAI | `src/providers/xai.ts` | Present with Responses and Chat selectors. | Needs package-parity review against the AI SDK xAI provider. |
|
||||
| GitHub Copilot | `src/providers/github-copilot.ts` | Present as explicit-base-URL OpenAI Chat/Responses facade. | Runtime/catalog integration remains specialized and should stay separate from public OpenAI-compatible defaults. |
|
||||
| Native slice | Source | Current state | Main gaps |
|
||||
| ---------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| OpenAI Chat | `src/protocols/openai-chat.ts`, `src/providers/openai.ts` | Usable. Streams text, reasoning deltas, tool calls, usage, images, and common generation controls. | No typed structured-output / `response_format` path. Limited typed OpenAI option surface compared with SDK escape hatches. |
|
||||
| OpenAI Responses HTTP | `src/protocols/openai-responses.ts`, `src/providers/openai.ts` | Usable. Supports hosted-tool event surfacing, reasoning replay metadata, GPT-5 defaults, and cache usage. | No explicit `previous_response_id` path. Typed options cover only a subset of Responses fields. Structured output is still mostly synthetic-tool based. |
|
||||
| OpenAI Responses WebSocket | `src/protocols/openai-responses.ts`, `src/route/transport/websocket.ts` | Present as `OpenAI.responsesWebSocket(...)`. | Runner/catalog support explicitly must not downgrade WebSocket routes; broader runtime selection is not complete. |
|
||||
| OpenAI-compatible Chat | `src/protocols/openai-compatible-chat.ts`, `src/providers/openai-compatible.ts` | Usable for generic Chat and several profiles: Baseten, Cerebras, DeepInfra, DeepSeek, Fireworks, Groq, TogetherAI. | No OpenAI-compatible Responses protocol/facade. Family quirks are mostly endpoint defaults, not full typed behavior. |
|
||||
| Anthropic Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic.ts` | Usable. Supports tools, thinking, cache control, images, server-hosted tool events, and usage. | Provider option surface is small. Beta/header handling, metadata, and newer Messages fields need a typed parity pass. |
|
||||
| Gemini Developer API | `src/protocols/gemini.ts`, `src/providers/google.ts` | Usable for Google API key flow. Supports text, images, tools, thinking signatures, and cache usage. | This is not Vertex. Typed provider options are narrow; many Gemini request fields currently require raw `http.body` overlays. |
|
||||
| Bedrock Converse | `src/protocols/bedrock-converse.ts`, `src/providers/amazon-bedrock.ts` | Partial but real. Supports AWS event-stream framing, SigV4 with supplied credentials, bearer auth, tools, reasoning signatures, media, cache points, and recorded tests. | Native facade does not mirror the AI SDK plugin's default AWS credential chain/profile behavior. Runner/catalog mapping is missing. Guardrails, inference profiles, region-specific model ID fixes, and model-specific request fields need a parity pass. |
|
||||
| Azure OpenAI | `src/providers/azure.ts` using OpenAI Chat/Responses protocols | Partial. Supports resource/base URL setup, API key auth, API version query, Chat, and Responses selectors. | Core runner does not map `@ai-sdk/azure` to this native facade. AAD/token auth and Azure-specific endpoint variants need review. |
|
||||
| Cloudflare AI Gateway / Workers AI | `src/providers/cloudflare.ts` | Present via OpenAI-compatible Chat routes. | Useful but not part of the critical AI SDK replacement set yet. Needs per-product recorded coverage before relying on it broadly. |
|
||||
| OpenRouter | `src/providers/openrouter.ts` | Present with OpenRouter-specific usage/reasoning/prompt-cache options over Chat. | Responses-style OpenRouter support is absent. |
|
||||
| xAI | `src/providers/xai.ts` | Present with Responses and Chat selectors. | Needs package-parity review against the AI SDK xAI provider. |
|
||||
| GitHub Copilot | `src/providers/github-copilot.ts` | Present as explicit-base-URL OpenAI Chat/Responses facade. | Runtime/catalog integration remains specialized and should stay separate from public OpenAI-compatible defaults. |
|
||||
|
||||
## V2 Runner Status
|
||||
|
||||
`packages/core/src/session/runner/model.ts` currently resolves only this native subset from catalog `aisdk` metadata:
|
||||
|
||||
| Catalog API | Native route used today |
|
||||
| --- | --- |
|
||||
| `aisdk:@ai-sdk/openai` | `OpenAIResponses.route` |
|
||||
| `aisdk:@ai-sdk/anthropic` | `AnthropicMessages.route` |
|
||||
| Catalog API | Native route used today |
|
||||
| --------------------------------------------------- | ---------------------------- |
|
||||
| `aisdk:@ai-sdk/openai` | `OpenAIResponses.route` |
|
||||
| `aisdk:@ai-sdk/anthropic` | `AnthropicMessages.route` |
|
||||
| `aisdk:@ai-sdk/openai-compatible` with explicit URL | `OpenAICompatibleChat.route` |
|
||||
|
||||
Everything else currently fails with `SessionRunnerModel.UnsupportedApiError` when the V2 native runner tries to resolve it. This includes `@ai-sdk/google`, `@ai-sdk/google-vertex`, `@ai-sdk/google-vertex/anthropic`, `@ai-sdk/azure`, `@ai-sdk/amazon-bedrock`, and `@ai-sdk/amazon-bedrock/mantle`.
|
||||
|
||||
## AI SDK Package Parity Matrix
|
||||
|
||||
| AI SDK package | Intended native target | Status | Biggest gaps |
|
||||
| --- | --- | --- | --- |
|
||||
| `@ai-sdk/openai` | `OpenAI.chat`, `OpenAI.responses`, `OpenAI.responsesWebSocket` | Partial / usable | Add complete typed option coverage, structured output strategy, explicit Responses continuation support, and runner route selection between Chat/Responses/WebSocket. |
|
||||
| `@ai-sdk/openai-compatible` | Generic OpenAI-compatible Chat plus future Responses | Partial | Add OpenAI-compatible Responses. Decide per-family namespace/profile behavior for providers that support Responses versus Chat only. |
|
||||
| `@ai-sdk/anthropic` | `AnthropicMessages` | Partial / usable | Finish Messages API parity for headers/betas/metadata/newer fields and document hosted-tool continuation expectations. |
|
||||
| `@ai-sdk/google` | Gemini Developer API | Partial / usable | Add typed options for safety, response schema/modalities, cached content, grounding/search/code execution, and non-text output modes where supported. |
|
||||
| `@ai-sdk/google-vertex` | Vertex Gemini namespace/facade | Missing | Implement Vertex endpoint derivation, ADC/OAuth auth, project/location/env resolution, OpenAI-compatible Vertex endpoint handling, and runner/catalog mapping. |
|
||||
| `@ai-sdk/google-vertex/anthropic` | Anthropic Messages over Vertex namespace/facade | Missing | Implement Vertex Anthropic endpoint/auth selection, regional endpoint behavior, and compatibility with Anthropic Messages lowering/parsing. |
|
||||
| `@ai-sdk/azure` | Azure OpenAI Chat/Responses facade | Partial | Map runner/catalog metadata to native Azure, handle resourceName/baseURL/apiVersion variants, add AAD/token auth story, and verify Chat vs Responses deployment selection. |
|
||||
| `@ai-sdk/amazon-bedrock` | Bedrock Converse | Partial | Add default AWS credential chain/profile support, region/inference-profile model ID handling, provider option parity via `additionalModelRequestFields`, guardrails/performance config, and runner/catalog mapping. |
|
||||
| `@ai-sdk/amazon-bedrock/mantle` | Bedrock Mantle OpenAI-compatible Chat/Responses namespace | Missing | Decide native Mantle shape, likely separate from Converse because it uses OpenAI-compatible Chat/Responses semantics over Bedrock. Add package mapping and tests. |
|
||||
| AI SDK package | Intended native target | Status | Biggest gaps |
|
||||
| --------------------------------- | -------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `@ai-sdk/openai` | `OpenAI.chat`, `OpenAI.responses`, `OpenAI.responsesWebSocket` | Partial / usable | Add complete typed option coverage, structured output strategy, explicit Responses continuation support, and runner route selection between Chat/Responses/WebSocket. |
|
||||
| `@ai-sdk/openai-compatible` | Generic OpenAI-compatible Chat plus future Responses | Partial | Add OpenAI-compatible Responses. Decide per-family namespace/profile behavior for providers that support Responses versus Chat only. |
|
||||
| `@ai-sdk/anthropic` | `AnthropicMessages` | Partial / usable | Finish Messages API parity for headers/betas/metadata/newer fields and document hosted-tool continuation expectations. |
|
||||
| `@ai-sdk/google` | Gemini Developer API | Partial / usable | Add typed options for safety, response schema/modalities, cached content, grounding/search/code execution, and non-text output modes where supported. |
|
||||
| `@ai-sdk/google-vertex` | Vertex Gemini namespace/facade | Missing | Implement Vertex endpoint derivation, ADC/OAuth auth, project/location/env resolution, OpenAI-compatible Vertex endpoint handling, and runner/catalog mapping. |
|
||||
| `@ai-sdk/google-vertex/anthropic` | Anthropic Messages over Vertex namespace/facade | Missing | Implement Vertex Anthropic endpoint/auth selection, regional endpoint behavior, and compatibility with Anthropic Messages lowering/parsing. |
|
||||
| `@ai-sdk/azure` | Azure OpenAI Chat/Responses facade | Partial | Map runner/catalog metadata to native Azure, handle resourceName/baseURL/apiVersion variants, add AAD/token auth story, and verify Chat vs Responses deployment selection. |
|
||||
| `@ai-sdk/amazon-bedrock` | Bedrock Converse | Partial | Add default AWS credential chain/profile support, region/inference-profile model ID handling, provider option parity via `additionalModelRequestFields`, guardrails/performance config, and runner/catalog mapping. |
|
||||
| `@ai-sdk/amazon-bedrock/mantle` | Bedrock Mantle OpenAI-compatible Chat/Responses namespace | Missing | Decide native Mantle shape, likely separate from Converse because it uses OpenAI-compatible Chat/Responses semantics over Bedrock. Add package mapping and tests. |
|
||||
|
||||
## Highest-Risk Gaps
|
||||
|
||||
|
|
@ -71,20 +70,20 @@ Everything else currently fails with `SessionRunnerModel.UnsupportedApiError` wh
|
|||
|
||||
These are implementation/API slices, not separate npm packages.
|
||||
|
||||
| API slice | Package-like entrypoint | Purpose |
|
||||
| --- | --- | --- |
|
||||
| OpenAI Chat | `@opencode-ai/llm/providers/openai/chat` | OpenAI `/chat/completions` semantics. |
|
||||
| OpenAI Responses | `@opencode-ai/llm/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. |
|
||||
| OpenAI-compatible Chat | `@opencode-ai/llm/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. |
|
||||
| OpenAI-compatible Responses | Missing | Generic OpenAI-compatible `/responses`. |
|
||||
| Anthropic Messages | `@opencode-ai/llm/providers/anthropic` | Anthropic Messages API. |
|
||||
| Gemini Developer API | `@opencode-ai/llm/providers/google` | Google AI Studio Gemini API. |
|
||||
| Vertex Gemini | Missing | Vertex Gemini API. |
|
||||
| Vertex Anthropic Messages | Missing | Vertex-hosted Anthropic Messages API. |
|
||||
| Bedrock Converse | `@opencode-ai/llm/providers/amazon-bedrock` | AWS Bedrock Converse API. |
|
||||
| Bedrock Mantle | Missing | AWS Bedrock Mantle OpenAI-compatible APIs. |
|
||||
| Azure OpenAI Chat | `@opencode-ai/llm/providers/azure/chat` | Azure specialization of OpenAI Chat. |
|
||||
| Azure OpenAI Responses | `@opencode-ai/llm/providers/azure/responses` | Azure specialization of OpenAI Responses. |
|
||||
| API slice | Package-like entrypoint | Purpose |
|
||||
| --------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| OpenAI Chat | `@opencode-ai/llm/providers/openai/chat` | OpenAI `/chat/completions` semantics. |
|
||||
| OpenAI Responses | `@opencode-ai/llm/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. |
|
||||
| OpenAI-compatible Chat | `@opencode-ai/llm/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. |
|
||||
| OpenAI-compatible Responses | Missing | Generic OpenAI-compatible `/responses`. |
|
||||
| Anthropic Messages | `@opencode-ai/llm/providers/anthropic` | Anthropic Messages API. |
|
||||
| Gemini Developer API | `@opencode-ai/llm/providers/google` | Google AI Studio Gemini API. |
|
||||
| Vertex Gemini | Missing | Vertex Gemini API. |
|
||||
| Vertex Anthropic Messages | Missing | Vertex-hosted Anthropic Messages API. |
|
||||
| Bedrock Converse | `@opencode-ai/llm/providers/amazon-bedrock` | AWS Bedrock Converse API. |
|
||||
| Bedrock Mantle | Missing | AWS Bedrock Mantle OpenAI-compatible APIs. |
|
||||
| Azure OpenAI Chat | `@opencode-ai/llm/providers/azure/chat` | Azure specialization of OpenAI Chat. |
|
||||
| Azure OpenAI Responses | `@opencode-ai/llm/providers/azure/responses` | Azure specialization of OpenAI Responses. |
|
||||
|
||||
## Suggested Next Work Slices
|
||||
|
||||
|
|
|
|||
42
specs/v2/README.md
Normal file
42
specs/v2/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# V2 Specifications
|
||||
|
||||
These documents explain V2 behavior that is difficult to recover from one source file. They are not API reference or a backlog.
|
||||
|
||||
## Authority
|
||||
|
||||
Authority follows the concern:
|
||||
|
||||
| Concern | Owner |
|
||||
| ------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| HTTP operations and transport errors | [Protocol](../../packages/protocol/src) endpoint definitions assembled by Server `HttpApi` |
|
||||
| Public domain shapes and durable event payloads | [Schema](../../packages/schema/src) |
|
||||
| Runtime behavior and persistence | [Core](../../packages/core/src) |
|
||||
| Canonical vocabulary and cross-domain invariants | Root [CONTEXT.md](../../CONTEXT.md) |
|
||||
| Contributor-critical regression guardrails | Root [AGENTS.md](../../AGENTS.md) |
|
||||
|
||||
Current specifications explain cross-module contracts without copying exact types. Decision records explain why a design was selected. Historical documents describe earlier states and may use obsolete names.
|
||||
|
||||
Generated clients follow the assembled public `HttpApi`. GitHub issues own active work; Git history preserves removed plans and scratchpads.
|
||||
|
||||
## Current Contracts
|
||||
|
||||
| Document | Job |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------- |
|
||||
| [Session](./session.md) | Explain prompt admission, execution, instructions, compaction, and recovery boundaries. |
|
||||
| [Tools](./tools.md) | Explain tool construction, registration, execution, and settlement laws. |
|
||||
|
||||
## Decisions And Proposals
|
||||
|
||||
| Document | Status | Job |
|
||||
| ----------------------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------- |
|
||||
| [Managed restart continuation](./session-restart-continuation.md) | Accepted and implemented | Record why graceful managed-service restart uses private Session suspension. |
|
||||
| [Provider policy](./provider-policy.md) | Proposed and unimplemented | Explore provider authorization independently from provider configuration. |
|
||||
|
||||
## Historical Context
|
||||
|
||||
| Document | Job |
|
||||
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| [Schema changelog](./schema-changelog.md) | Preserve the pre-release compatibility ledger. Names in older entries are intentionally historical. |
|
||||
| [Catalog/config/plugin lifecycle](./catalog-config-plugin-lifecycle.md) | Preserve the option comparison that led to replayable Location-scoped catalog transforms. |
|
||||
|
||||
Do not add implementation checklists here. Put actionable work in GitHub issues and package-specific contributor guidance next to the code it governs.
|
||||
1153
specs/v2/api.html
1153
specs/v2/api.html
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
# Catalog / Config / Plugin Lifecycle Options
|
||||
|
||||
Status: current core has selected replayable Location-scoped Catalog transforms, aligned with option B. Reload/watch behavior and deferred external plugin activation remain design work; the option comparison below is retained as historical context.
|
||||
Status: **Historical decision record.** Option B, replayable Location-scoped Catalog transforms, was selected and implemented. All interfaces and flows below are historical option sketches, not current API reference; current behavior is owned by Core.
|
||||
|
||||
We need to choose where provider/model inputs live and how visible catalog state changes after boot. The designs below compare config, models.dev, auth, plugin activation/disablement, config edits, and policy changes under each option.
|
||||
The decision compared where provider/model inputs live and how visible catalog state changes after boot. The designs below preserve that comparison.
|
||||
|
||||
## Scenarios
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ We need to choose where provider/model inputs live and how visible catalog state
|
|||
- Config edit: authored configuration changes while the location is open.
|
||||
- Policy: allowed/denied provider selection changes after providers exist.
|
||||
|
||||
## A. Config Transforms, Service Reload
|
||||
## A. Rejected: Config Transforms, Service Reload
|
||||
|
||||
`Config` merges its ordered documents and then runs ordered, replayable plugin transforms. Each transform is a callback receiving `Draft<Config.Info>` and may mutate any config field.
|
||||
|
||||
|
|
@ -27,13 +27,9 @@ const transform = yield * Config.transform()
|
|||
yield *
|
||||
transform((config) => {
|
||||
config.providers ??= {}
|
||||
config.providers.acme = {
|
||||
/* ... */
|
||||
}
|
||||
config.providers.acme = {/* ... */}
|
||||
config.model = "acme/code"
|
||||
config.permissions = [
|
||||
/* ... */
|
||||
]
|
||||
config.permissions = [/* ... */]
|
||||
})
|
||||
```
|
||||
|
||||
|
|
@ -188,7 +184,7 @@ policy config changes
|
|||
- One reload produces at most one `Catalog.Event.Updated` notification.
|
||||
- Deferred plugin activation avoids blocking readiness, but plugin completions may cause repeated full-service reload batches during startup.
|
||||
|
||||
## B. Catalog Transforms
|
||||
## B. Selected: Catalog Transforms
|
||||
|
||||
Plugins register replayable catalog transforms. Each transform receives a `Catalog.Editor` whose helper methods mutate a private catalog draft; `Catalog` rematerializes visible records from its active transforms.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,399 +0,0 @@
|
|||
# V2 Config Review
|
||||
|
||||
This document breaks the legacy configuration schema into small review groups. Work through one group at a time and decide whether each field should be ported as-is, removed, or redesigned for v2.
|
||||
|
||||
## Status Labels
|
||||
|
||||
- `pending`: not discussed yet
|
||||
- `keep`: port with substantially the existing meaning
|
||||
- `remove`: do not carry forward
|
||||
- `redesign`: keep the capability with a different shape, scope, or owning module
|
||||
|
||||
## Schema Scope
|
||||
|
||||
Use one v2 config schema for now. Some fields, such as `autoupdate`, are intended for global/user configuration, but there is not yet enough benefit to enforce that with separate global and location schemas. Revisit this if more scope-sensitive fields survive the review.
|
||||
|
||||
V2 core discovers config documents named `opencode.json` or `opencode.jsonc` in the global config directory, ancestor project directories, and `.opencode` config directories. The legacy `config.json` filename is not supported in V2.
|
||||
|
||||
## Group 1: File Metadata
|
||||
|
||||
Small fields describing the config file itself rather than application behavior.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| --------- | ---------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------- |
|
||||
| `$schema` | JSON schema reference for editor validation and completion | keep | Keep as read-only metadata; loading config must not insert it or create files for it. |
|
||||
|
||||
## Group 2: Process And Server Settings
|
||||
|
||||
Settings that affect process startup, shell execution, or network serving. Review global-only versus location-specific scope carefully.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------ | --------------------------------------------------- | ------ | ------------------------------------------------------------------------------ |
|
||||
| `shell` | Default shell for terminal and shell tool execution | keep | Port as effective config; shared shell choice is used throughout opencode. |
|
||||
| `logLevel` | Intended logging level configuration | remove | Do not port: no config consumer exists and logging initializes from CLI input. |
|
||||
| `server` | Hostname, port, mDNS, and CORS settings | remove | Do not port: location config is loaded after the server is already running. |
|
||||
| `autoupdate` | Automatic update or notification behavior | keep | Global-only user preference; keep `true`, `false`, and `"notify"`. |
|
||||
|
||||
## Group 3: Commands And Project Resources
|
||||
|
||||
Configuration that introduces location-scoped project resources or discoverable content.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| -------------- | --------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `command` | User-defined commands | remove | Do not port as v2 config; named reusable user workflows belong to skills. |
|
||||
| `skills` | Additional skill locations | redesign | Replace `{ paths?, urls? }` with a single array of local path or remote URL discovery sources. |
|
||||
| `reference` | Named git or local directory references | redesign | Rename to plural `references`; retain named local path and Git repository external-context entries. |
|
||||
| `instructions` | Additional ambient instruction sources | keep | Keep as one array of local paths, glob patterns, or remote URLs supplying automatically included context. |
|
||||
|
||||
V2 does not expose separate user-authored command configuration. Skills should cover named reusable prompt workflows, whether invoked directly by the user or loaded by an agent. Internal command routing and built-in commands may remain runtime concerns without creating a `command` or `commands` config field.
|
||||
|
||||
This intentionally does not port legacy command-only behavior such as per-command `model`, `agent`, `subtask`, prompt shell expansion, or positional/template substitution. If a related capability is needed in v2, it should be designed in the owning domain rather than preserved through a second workflow definition system.
|
||||
|
||||
Keep `skills` as discovery-source configuration rather than inline workflow definitions. Skill content remains owned by `SKILL.md`; each `skills` entry is either a local search root or a remote discovery URL. Direct invocation behavior can be designed separately without expanding the config shape.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"skills": ["./team-skills", "~/shared-skills", "https://example.com/.well-known/skills/"],
|
||||
}
|
||||
```
|
||||
|
||||
Keep ambient instructions separate from skills. Instructions are automatically included as model context, while skills are loaded or invoked intentionally. Each source is unambiguously either a local path/glob or a URL, so v2 keeps the simple array shape:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"instructions": [
|
||||
"CONTRIBUTING.md",
|
||||
"docs/guidelines.md",
|
||||
".cursor/rules/*.md",
|
||||
"https://example.com/shared-rules.md",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Keep named external context references as a v2 configuration capability, renamed to plural `references` because it is a collection keyed by alias. References declare local directories or Git repositories that can later be addressed as `@alias` or `@alias/path` when the v2 runtime implements this behavior.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"references": {
|
||||
"design-system": { "path": "../ui-library" },
|
||||
"sdk": { "repository": "github.com/example/sdk", "branch": "main" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Retain the compact string entry form as well: values starting with `.`, `/`, or `~` represent local paths, and other strings represent Git repositories.
|
||||
|
||||
## Group 4: Plugins
|
||||
|
||||
Plugin loading has source-path and scope-sensitive behavior, so it should be reviewed separately from other project resources.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| -------- | ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `plugin` | User-specified plugin modules | redesign | Rename to plural `plugins`; retain ordered loading with package strings or `{ package, options? }` entries. |
|
||||
|
||||
Plugin order remains part of the v2 configuration contract because hook registration and execution can depend on load order. Replace legacy option tuples with readable object entries:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"plugins": [
|
||||
"opencode-helicone-session",
|
||||
{
|
||||
"package": "@my-org/audit-plugin",
|
||||
"options": {
|
||||
"endpoint": "https://audit.example.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The configured `plugins` list represents package-loaded plugins only. Local plugin code remains discovered from plugin directories such as `.opencode/plugins/`; v2 does not port arbitrary configured local paths or file URLs into this field.
|
||||
|
||||
## Group 5: Filesystem And Tool Runtime
|
||||
|
||||
Settings controlling local file observation, snapshots, language tooling, and tool output behavior.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------- | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `watcher` | Ignore patterns for filesystem watching | keep | Keep `{ ignore?: string[] }`; this configures the filesystem watcher subsystem. |
|
||||
| `snapshot` | Enable filesystem snapshot tracking | redesign | Rename to plural `snapshots`; controls creation of snapshots used for undo and revert behavior. |
|
||||
| `formatter` | Configure formatters | keep | Keep singular `boolean \| Record<string, entry>` shape; it configures built-in enablement and named formatter overrides. |
|
||||
| `lsp` | Configure language servers | keep | Keep singular `boolean \| Record<string, entry>` shape; custom servers need commands and file extensions. |
|
||||
| `attachment` | Configure attachment/image processing | redesign | Rename to plural `attachments`; retain `{ image?: { auto_resize?, max_width?, max_height?, max_base64_bytes? } }` for input normalization limits. |
|
||||
| `tool_output` | Configure tool output truncation limits | keep | Keep `{ max_lines?, max_bytes? }`; both positive thresholds apply to saved-preview truncation behavior. |
|
||||
|
||||
`formatter` and `lsp` configure one project tooling subsystem each, so their singular names remain appropriate. `true` enables the built-in registrations, `false` disables them, and a keyed object enables built-ins while applying named overrides or custom registrations. Custom language servers must declare `extensions` so runtime file attachment is deterministic; validation of known built-in server IDs belongs with the eventual v2 LSP integration rather than the aggregate core config schema.
|
||||
|
||||
Rename legacy `attachment` to `attachments` in v2. This setting controls processing for the attachment domain and may expand beyond image handling, while singular `attachment` is already used as a model capability flag indicating whether one model accepts attachments.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"formatter": {
|
||||
"prettier": { "disabled": true },
|
||||
"project": { "command": ["./scripts/format", "$FILE"], "extensions": [".foo"] },
|
||||
},
|
||||
"lsp": {
|
||||
"typescript": { "disabled": true },
|
||||
"project": { "command": ["project-language-server", "--stdio"], "extensions": [".foo"] },
|
||||
},
|
||||
"attachments": {
|
||||
"image": { "auto_resize": true, "max_width": 2000, "max_height": 2000 },
|
||||
},
|
||||
"tool_output": { "max_lines": 2000, "max_bytes": 51200 },
|
||||
}
|
||||
```
|
||||
|
||||
## Group 6: Sharing And Identity
|
||||
|
||||
Settings affecting sharing behavior or user/account identity rather than model execution.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------ | ----------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `share` | Session sharing behavior | keep | Keep `"manual" \| "auto" \| "disabled"`; it controls manual sharing permission and automatic sharing of new sessions. |
|
||||
| `autoshare` | Legacy automatic sharing flag | remove | Do not port deprecated alias; use `share: "auto"`. |
|
||||
| `enterprise` | Enterprise URL configuration | keep | Keep `{ url?: string }`; currently selects the legacy sharing service endpoint when no organization account is active. |
|
||||
| `username` | Display username in conversations and telemetry | keep | Keep string identity override; runtime may otherwise resolve an operating-system username. |
|
||||
|
||||
Retain `share` as the single session-sharing setting. `"manual"` permits explicit sharing, `"auto"` shares newly created top-level sessions, and `"disabled"` prevents sharing. Legacy `autoshare: true` is only an alias for `share: "auto"`, so v2 does not expose it.
|
||||
|
||||
Retain `enterprise.url` for legacy enterprise share hosting selection and `username` as a user-facing identity override. These remain separate from server authentication credentials; `username` identifies the user in conversation and telemetry behavior rather than HTTP basic-auth configuration.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"share": "disabled",
|
||||
"enterprise": { "url": "https://share.example.com" },
|
||||
"username": "developer",
|
||||
}
|
||||
```
|
||||
|
||||
## Group 7: Providers And Model Selection
|
||||
|
||||
Provider catalog customization and model-choice configuration. The new core work has started here.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| -------------------- | ------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `provider` | Custom provider configuration and model overrides | redesign | Rename to plural `providers` in v2; do not preserve the legacy singular key. Review nested provider/model fields separately. |
|
||||
| `disabled_providers` | Disable automatically loaded providers | redesign | Replace with `experimental.policies: [{ effect: "deny", action: "provider.use", resource: "..." }]`. |
|
||||
| `enabled_providers` | Restrict enabled providers to an allowlist | redesign | Replace with ordered `provider.use` allow/deny statements and wildcard resources. |
|
||||
| `model` | Default model selection | keep | Keep as the fallback model when an active session or agent does not specify a model. |
|
||||
| `small_model` | Small/utility model selection | remove | Do not port; its only runtime consumer is title generation, which can use an explicit `title` agent model override. |
|
||||
|
||||
Provider selection rules belong in `experimental.policies` rather than provider entries or repeated top-level provider fields. Initial proposed shape:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"experimental": {
|
||||
"policies": [
|
||||
{
|
||||
"effect": "deny",
|
||||
"action": "provider.use",
|
||||
"resource": "*",
|
||||
},
|
||||
{
|
||||
"effect": "allow",
|
||||
"action": "provider.use",
|
||||
"resource": "anthropic",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See [provider-policy.md](./provider-policy.md) for the provider policy semantics and precedence rules.
|
||||
|
||||
Policy evaluation will consume authored config documents in reverse order while preserving statement order inside each document. The precedence of `.opencode` policy sources remains open until `.opencode` configuration is reviewed.
|
||||
|
||||
Provider configuration uses the plural `providers` key in v2. This intentionally differs from the legacy singular `provider` key; v2 does not add a compatibility alias while its configuration surface is still being defined.
|
||||
|
||||
Keep `model` as the default model fallback. It is application-wide behavior used when an active session or agent has no explicit model selection, so it does not belong inside any individual provider configuration.
|
||||
|
||||
Do not port `small_model`. In the current runtime it is only consulted while generating a session title: the `title` agent model wins first, then `small_model`, then automatic/current-model fallback. In v2, users who need a specific title model should configure the `title` agent directly rather than use a separate top-level model setting.
|
||||
|
||||
Provider, model, variant, and provisional agent `options` are authored as partial patches rather than fully materialized runtime option records. Users should be able to set only the override they need, such as a header or an AI SDK request option; catalog state supplies empty defaults and merges patches in configuration order.
|
||||
|
||||
Keep provider `env` as an authored list of recognized credential environment variable names. Built-in catalog providers already carry this metadata for automatic environment-backed availability, and configured providers may need to declare the same source. For a configured provider this is additive metadata, not a requirement that one of the variables exists: the provider may instead be usable through configured options, a stored account, or an endpoint that needs no credential.
|
||||
|
||||
Within configured models, nest the legacy upstream model identifier `id` under `api.id` with the rest of the model API override. Model `limit` is an authored patch, so an override may change only `context`, `input`, or `output`. Model `cost` accepts one simple pricing object or an array of tiered pricing entries; omitted cache prices default to zero.
|
||||
|
||||
Do not port legacy provider model `reasoning`, `temperature`, or `interleaved` flags as first-class config fields; provider/request behavior belongs in structured `options` or model variants. Do not port `release_date`, `status`, `experimental`, `whitelist`, or `blacklist` in this v2 surface.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"providers": {
|
||||
"internal": {
|
||||
"env": ["INTERNAL_LLM_API_KEY"],
|
||||
"options": { "headers": { "Authorization": "Bearer {env:API_KEY}" } },
|
||||
"models": {
|
||||
"chat": {
|
||||
"api": { "id": "upstream-chat-model" },
|
||||
"limit": { "output": 32768 },
|
||||
"cost": { "input": 1.25, "output": 10 },
|
||||
"variants": [{ "id": "high", "aisdk": { "request": { "reasoningEffort": "high" } } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Group 8: Agents And Permissions
|
||||
|
||||
Agent behavior and tool-access policy. Review together because agent configuration can contain permissions and model choices.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| --------------- | --------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `default_agent` | Choose default primary agent | remove | Do not retain a separate top-level selector; default choice should be designed with the v2 agent configuration model. |
|
||||
| `mode` | Legacy agent configuration alias | remove | Do not port deprecated alias; configure agents through the v2 agent surface only. |
|
||||
| `agent` | Configure primary, subagent, and specialized agents | redesign | Rename to plural `agents`; retain a named map of built-in overrides and custom agent definitions. |
|
||||
| `permission` | Tool permission rules | redesign | Rename to plural `permissions`; replace legacy map shorthand with an ordered array of `{ action, resource, effect }` rules. |
|
||||
| `tools` | Legacy tool enable/disable map | remove | Do not port boolean enable/disable alias; express tool access through permissions. |
|
||||
|
||||
Do not port `default_agent` ahead of the v2 agent design. The legacy runtime uses it to choose a visible, non-subagent fallback instead of `build`, but exposing that selection as an isolated top-level field would pre-commit v2 to the legacy agent model before agents and their policy surface are defined together.
|
||||
|
||||
Do not port `mode`. The legacy loader already merges this deprecated alias into `agent`, and v2 should expose only one authoring surface for agent definitions.
|
||||
|
||||
Rename legacy `agent` to `agents` because the setting is a collection keyed by agent name. It should continue to support overriding built-in agents such as `build`, `plan`, and `title`, as well as declaring named custom agents. The nested entry schema remains open until agent-local `permission` and deprecated `tools` behavior are decided.
|
||||
|
||||
Keep nested `agents.<name>.mode` with values `"primary"`, `"subagent"`, or `"all"`. This identifies an agent's runtime role and is separate from the removed top-level legacy `mode` alias, which was an alternate container for agent definitions.
|
||||
|
||||
For named configurable entries across v2, use `disabled?: boolean` consistently when an entry should remain configured but inactive. Agent definitions should therefore redesign legacy `disable` as `disabled`; this matches formatters, language servers, future MCP server definitions, and configured model overrides. Runtime catalog state may still track active availability as `enabled`; that is not user-authored config.
|
||||
|
||||
Keep separate `model` and `variant` fields on agent definitions. A model reference uses `provider/model-id`, but model IDs may themselves contain slash-delimited segments, such as `openrouter/openai/gpt-5`; appending a variant to that string would be ambiguous.
|
||||
|
||||
Keep `color` on agent definitions. Agents are user-visible selectable entities, so a user-authored display color is appropriate metadata for the agent rather than an unrelated application presentation setting. Retain hex colors and named theme colors supported by the existing configuration.
|
||||
|
||||
Keep agent-local `options` provisionally using the same structured provider options shape available on configured providers and models: headers, body, and AI SDK provider/request overrides. Its long-term ownership remains open for team review because reusable provider-specific presets can instead be modeled as variants. Do not retain dedicated agent `temperature` or `top_p` fields.
|
||||
|
||||
Retain `description`, `hidden`, and `steps`; they define an agent's discoverability, visibility, and iteration budget rather than model request parameters. Rename legacy agent `prompt` to `system`, making clear that it supplies persistent system-level agent content without colliding with top-level ambient `instructions`. Remove deprecated `maxSteps` in favor of `steps`.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"reviewer": {
|
||||
"model": "openrouter/openai/gpt-5",
|
||||
"variant": "high",
|
||||
"options": {
|
||||
"headers": { "x-agent": "reviewer" },
|
||||
"body": {},
|
||||
"aisdk": { "provider": {}, "request": { "reasoningEffort": "high" } },
|
||||
},
|
||||
"description": "Review changes for correctness",
|
||||
"system": "Find regressions and missing tests.",
|
||||
"mode": "subagent",
|
||||
"color": "warning",
|
||||
"steps": 12,
|
||||
"disabled": false,
|
||||
"permissions": [{ "action": "edit", "resource": "*", "effect": "deny" }],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Do not port `tools`, either as a top-level setting or as an agent-entry alias. The legacy loader already converts tool booleans into permission rules, including collapsing write-adjacent tool names into `edit`; v2 should avoid carrying that lossy compatibility input forward.
|
||||
|
||||
Rename legacy `permission` to `permissions` and expose the normalized ordered ruleset already modeled by `PermissionV2.Ruleset`. Rules retain the interactive `"ask"` effect in addition to `"allow"` and `"deny"`; this is distinct from `experimental.policies`, whose provider enforcement currently needs only allow/deny decisions. The same `permissions` ruleset shape should be used inside future `agents` entries.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"permissions": [
|
||||
{ "action": "bash", "resource": "*", "effect": "ask" },
|
||||
{ "action": "bash", "resource": "git status", "effect": "allow" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Group 9: Integrations
|
||||
|
||||
External protocol and server integration configuration.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ----- | ------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mcp` | MCP server definitions and enablement | redesign | Keep opencode's explicit local/remote server entry format, nested under `mcp.servers`; use `disabled` for inactive entries and move timeout defaults here. |
|
||||
|
||||
Keep the opencode MCP server entry format instead of adopting the common `mcpServers` copy/paste shape. Local servers remain explicit `type: "local"` entries with command arrays and `environment`; remote servers remain explicit `type: "remote"` entries with `url`, `headers`, and optional `oauth`. Nest the server map under `mcp.servers` so protocol-wide settings such as timeout defaults can live under the same subsystem.
|
||||
|
||||
MCP timeouts have separate startup, catalog, and execution budgets, expressed in milliseconds. `startup` covers establishing the transport and completing MCP initialization. `catalog` applies independently to discovery requests such as listing tools and prompts. `execution` covers potentially interactive operations such as tool calls and prompt evaluation. A server may override any default without repeating the others.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mcp": {
|
||||
"timeout": { "startup": 30000, "catalog": 30000, "execution": 43200000 },
|
||||
"servers": {
|
||||
"github": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "@github/github-mcp-server"],
|
||||
"environment": { "GITHUB_TOKEN": "{env:GITHUB_TOKEN}" },
|
||||
"disabled": false,
|
||||
"timeout": { "startup": 60000 },
|
||||
},
|
||||
"docs": {
|
||||
"type": "remote",
|
||||
"url": "https://docs.example.com/mcp",
|
||||
"headers": { "Authorization": "Bearer {env:DOCS_TOKEN}" },
|
||||
"oauth": {
|
||||
"client_id": "{env:MCP_CLIENT_ID}",
|
||||
"client_secret": "{env:MCP_CLIENT_SECRET}",
|
||||
"scope": "read write",
|
||||
"callback_port": 19876,
|
||||
"redirect_uri": "http://127.0.0.1:19876/mcp/oauth/callback",
|
||||
},
|
||||
"disabled": false,
|
||||
"timeout": { "execution": 600000 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Group 10: Conversation Lifecycle
|
||||
|
||||
Behavior affecting long-running conversations and context management.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------ | ----------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------- |
|
||||
| `compaction` | Automatic compaction, pruning, and context reserve settings | redesign | Group retained verbatim history under `keep` and rename context headroom to `buffer`. |
|
||||
|
||||
Retain the compaction capability but redesign the less clear limits. `keep.tokens` is the token budget for recent history serialized into the textual compaction checkpoint. `buffer` is the token headroom reserved so automatic compaction triggers before the input window is exhausted.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"compaction": {
|
||||
"auto": true,
|
||||
"prune": true,
|
||||
"keep": {
|
||||
"tokens": 2000,
|
||||
},
|
||||
"buffer": 10000,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Group 11: Deprecated And Experimental Settings
|
||||
|
||||
Fields that should not be ported by inertia; each needs an explicit justification.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------------------------------ | --------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `layout` | Legacy layout selection | remove | Do not port deprecated option; stretch layout is always used. |
|
||||
| `experimental.disable_paste_summary` | Disable pasted-content summary behavior | remove | Do not port; pasted-input presentation behavior belongs to the client/UI surface. |
|
||||
| `experimental.batch_tool` | Enable batch tool | remove | Do not port; batch tool is no longer a supported feature. |
|
||||
| `experimental.openTelemetry` | Enable AI SDK telemetry spans | remove | Do not port; observability is process-level and should use standard OpenTelemetry environment or declarative configuration. |
|
||||
| `experimental.primary_tools` | Restrict tools to primary agents | remove | Do not port obsolete gating; agent tool access is configured through permissions. |
|
||||
| `experimental.continue_loop_on_deny` | Continue loop after denied tool call | remove | Do not port legacy denied-tool loop behavior. |
|
||||
| `experimental.mcp_timeout` | MCP request timeout | redesign | Migrate to both `mcp.timeout.catalog` and `mcp.timeout.execution`, with corresponding per-server overrides. |
|
||||
|
||||
## Review Order
|
||||
|
||||
Work through the groups in this order unless a dependency between decisions becomes clear:
|
||||
|
||||
1. File Metadata
|
||||
2. Process And Server Settings
|
||||
3. Providers And Model Selection
|
||||
4. Commands And Project Resources
|
||||
5. Plugins
|
||||
6. Filesystem And Tool Runtime
|
||||
7. Sharing And Identity
|
||||
8. Agents And Permissions
|
||||
9. Integrations
|
||||
10. Conversation Lifecycle
|
||||
11. Deprecated And Experimental Settings
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
# V2 Core Instructions
|
||||
|
||||
These notes describe how to work on `packages/core` during the v2 port.
|
||||
|
||||
## Direction
|
||||
|
||||
Move behavior out of large application services and into plugins. Core services should become small, typed containers that own state, expose simple operations, and trigger hooks where policy or integration-specific logic belongs.
|
||||
|
||||
The target shape is:
|
||||
|
||||
- `packages/core` contains domain schemas, typed errors, state containers, events, and plugin hook contracts.
|
||||
- Plugins implement provider-specific, config-specific, auth-specific, model-discovery, and generation behavior.
|
||||
- Services are hot-reloadable by design: updates are granular, observable, and do not require tearing down the whole process.
|
||||
- `packages/opencode` becomes thinner over time: UI, server routes, CLI, storage glue, and legacy compatibility should call the core services instead of owning domain logic directly.
|
||||
|
||||
## Service Shape
|
||||
|
||||
Core services should look like `Catalog`, `AccountV2`, and `AgentV2`:
|
||||
|
||||
- define schemas and branded ids at the top of the module
|
||||
- define typed `Schema.TaggedErrorClass` errors for expected failures
|
||||
- define an `Interface` with small operations
|
||||
- expose a `Context.Service`
|
||||
- implement `layer` with private in-memory state
|
||||
- expose `defaultLayer` with explicit dependencies
|
||||
- self-export with `export * as Name from "./file"`
|
||||
|
||||
Prefer a dumb container API:
|
||||
|
||||
- `get`, `all`, `available`, `default`, `update`, `remove`, `activate`, or other small domain verbs
|
||||
- `update(id, draft => ...)` for registration and mutation
|
||||
- hook calls before committing mutations when plugins need to enrich, cancel, or validate changes
|
||||
- events after committing mutations when other services or frontends need to react
|
||||
|
||||
Avoid putting application policy directly in core services unless it is a domain invariant. For example, resolving model endpoint inheritance is catalog-owned; deciding which providers to register is plugin-owned.
|
||||
|
||||
## Plugin Hooks
|
||||
|
||||
Plugins are the extension boundary for v2. Add hooks to `PluginV2.HookSpec` when logic should be provided by integrations instead of the container itself.
|
||||
|
||||
Hook conventions:
|
||||
|
||||
- hooks receive immutable input plus mutable output
|
||||
- mutable object outputs are exposed as Immer drafts
|
||||
- include `cancel: boolean` when plugins can prevent a mutation
|
||||
- trigger hooks sequentially so ordering remains deterministic
|
||||
- keep hook names domain-oriented, like `provider.update`, `model.update`, `account.activate`, `agent.generate`
|
||||
- keep hook payloads small and typed with core schemas
|
||||
|
||||
Use hooks for:
|
||||
|
||||
- registering providers and models
|
||||
- applying env/account/config-derived enablement
|
||||
- transforming SDK/provider options
|
||||
- implementing generated behavior such as agent generation
|
||||
- choosing defaults when the choice is policy rather than state
|
||||
|
||||
Do not use hooks as a dumping ground for transport concerns, UI behavior, or compatibility shims.
|
||||
|
||||
## Plugin Boot
|
||||
|
||||
Built-in core plugins are registered by `packages/core/src/plugin/boot.ts`.
|
||||
|
||||
When a new core service is intended to be available to plugins:
|
||||
|
||||
- add the service to the boot layer dependency type
|
||||
- yield the service inside the layer
|
||||
- provide it to each plugin effect in `add`
|
||||
- add its default layer to `PluginBoot.defaultLayer` only when that does not create a cycle
|
||||
|
||||
Keep boot as composition only. It should not contain provider, account, agent, or model policy itself.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Core should not import from `packages/opencode`. If a type or concept is needed by core, move or remodel the domain shape in core first.
|
||||
|
||||
Avoid moving legacy services over wholesale. Port the domain shape and the container API, then leave specific behavior behind hooks for plugins to implement.
|
||||
|
||||
When porting an opencode service:
|
||||
|
||||
- identify the state it owns
|
||||
- identify the operations callers actually need
|
||||
- identify which branches are policy or integration behavior
|
||||
- model state and operations in `packages/core`
|
||||
- add hooks for the policy/integration branches
|
||||
- keep old package code working until callers can migrate incrementally
|
||||
|
||||
## Schemas And Types
|
||||
|
||||
Use Effect schemas as the public contract:
|
||||
|
||||
- branded schemas for ids
|
||||
- `Schema.Class` or `Schema.Struct` for domain data
|
||||
- `Schema.TaggedErrorClass` for expected errors
|
||||
- existing core helpers like `DeepMutable`, `statics`, and integer schemas where appropriate
|
||||
|
||||
Prefer `Info` objects as the stored domain records. Add static `empty(...)` constructors when update APIs need to create records on first mutation.
|
||||
|
||||
Keep schemas stable and explicit. Do not rely on opencode config shapes as core domain shapes unless the config shape is actually the domain model.
|
||||
|
||||
## State And Events
|
||||
|
||||
Keep state private to the service layer. Use immutable replacement or Effect refs when persistence/concurrency requires it.
|
||||
|
||||
Publish events for committed domain changes, not for attempted mutations. Event names should describe domain facts, for example `catalog.model.updated`.
|
||||
|
||||
The v2 goal is granular reconfiguration. A model update should let dependents react to that model update; it should not require global reloads.
|
||||
|
||||
## Style
|
||||
|
||||
Follow the local core style:
|
||||
|
||||
- `Effect.gen(function* () { ... })` for composition
|
||||
- `Effect.fn("Domain.method")` for public service methods
|
||||
- `Effect.fnUntraced` for small internal mutation helpers
|
||||
- `yield* new ErrorClass(...)` for typed failures
|
||||
- minimal helpers unless they name a real concept
|
||||
- no `any` unless an existing plugin boundary requires it
|
||||
- no compatibility code without a concrete persisted or external-consumer need
|
||||
|
||||
Prefer the smallest correct port. The goal is to make services easier to replace and reason about, not to recreate the old architecture in a new package.
|
||||
|
|
@ -1,400 +0,0 @@
|
|||
# Provider and Model Catalog
|
||||
|
||||
## Provider Schema
|
||||
|
||||
```ts
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("ProviderV2.ID"),
|
||||
statics((schema) => ({
|
||||
opencode: schema.make("opencode"),
|
||||
anthropic: schema.make("anthropic"),
|
||||
openai: schema.make("openai"),
|
||||
google: schema.make("google"),
|
||||
googleVertex: schema.make("google-vertex"),
|
||||
githubCopilot: schema.make("github-copilot"),
|
||||
amazonBedrock: schema.make("amazon-bedrock"),
|
||||
azure: schema.make("azure"),
|
||||
openrouter: schema.make("openrouter"),
|
||||
mistral: schema.make("mistral"),
|
||||
gitlab: schema.make("gitlab"),
|
||||
})),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
const OpenAIResponses = Schema.Struct({
|
||||
type: Schema.Literal("openai/responses"),
|
||||
url: Schema.String,
|
||||
websocket: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
|
||||
const OpenAICompletions = Schema.Struct({
|
||||
type: Schema.Literal("openai/completions"),
|
||||
url: Schema.String,
|
||||
reasoning: Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("reasoning_content"),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("reasoning_details"),
|
||||
}),
|
||||
]).pipe(Schema.optional),
|
||||
})
|
||||
export type OpenAICompletions = typeof OpenAICompletions.Type
|
||||
|
||||
const AISDK = Schema.Struct({
|
||||
type: Schema.Literal("aisdk"),
|
||||
package: Schema.String,
|
||||
url: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const AnthropicMessages = Schema.Struct({
|
||||
type: Schema.Literal("anthropic/messages"),
|
||||
url: Schema.String,
|
||||
})
|
||||
|
||||
const UnknownEndpoint = Schema.Struct({
|
||||
type: Schema.Literal("unknown"),
|
||||
})
|
||||
|
||||
export const Endpoint = Schema.Union([
|
||||
UnknownEndpoint,
|
||||
OpenAIResponses,
|
||||
OpenAICompletions,
|
||||
AnthropicMessages,
|
||||
AISDK,
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Endpoint = typeof Endpoint.Type
|
||||
|
||||
export const Options = Schema.Struct({
|
||||
headers: Schema.Record(Schema.String, Schema.String),
|
||||
body: Schema.Record(Schema.String, Schema.Any),
|
||||
aisdk: Schema.Struct({
|
||||
provider: Schema.Record(Schema.String, Schema.Any),
|
||||
request: Schema.Record(Schema.String, Schema.Any),
|
||||
}),
|
||||
})
|
||||
export type Options = typeof Options.Type
|
||||
|
||||
export class Info extends Schema.Class<Info>("ProviderV2.Info")({
|
||||
id: ID,
|
||||
name: Schema.String,
|
||||
enabled: Schema.Union([
|
||||
Schema.Literal(false),
|
||||
Schema.Struct({ via: Schema.Literal("env"), name: Schema.String }),
|
||||
Schema.Struct({ via: Schema.Literal("account"), service: Schema.String }),
|
||||
Schema.Struct({ via: Schema.Literal("custom"), data: Schema.Record(Schema.String, Schema.Any) }),
|
||||
]),
|
||||
env: Schema.String.pipe(Schema.Array),
|
||||
endpoint: Endpoint,
|
||||
options: Options,
|
||||
}) {
|
||||
static empty(providerID: ID) {
|
||||
return new Info({
|
||||
id: providerID,
|
||||
name: providerID,
|
||||
enabled: false,
|
||||
env: [],
|
||||
endpoint: {
|
||||
type: "unknown",
|
||||
},
|
||||
options: {
|
||||
headers: {},
|
||||
body: {},
|
||||
aisdk: { provider: {}, request: {} },
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFound extends Schema.TaggedErrorClass<NotFound>("ProviderV2.NotFound")("ProviderV2.NotFound", {
|
||||
providerID: ID,
|
||||
}) {}
|
||||
```
|
||||
|
||||
## Model Schema
|
||||
|
||||
```ts
|
||||
export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const VariantID = Schema.String.pipe(Schema.brand("VariantID"))
|
||||
export type VariantID = typeof VariantID.Type
|
||||
|
||||
export const Family = Schema.String.pipe(Schema.brand("Family"))
|
||||
export type Family = typeof Family.Type
|
||||
|
||||
export const Capabilities = Schema.Struct({
|
||||
tools: Schema.Boolean,
|
||||
input: Schema.String.pipe(Schema.Array),
|
||||
output: Schema.String.pipe(Schema.Array),
|
||||
})
|
||||
export type Capabilities = typeof Capabilities.Type
|
||||
|
||||
export const Variant = Schema.Struct({
|
||||
id: VariantID,
|
||||
...ProviderV2.Options.fields,
|
||||
})
|
||||
export type Variant = typeof Variant.Type
|
||||
|
||||
export const Cost = Schema.Struct({
|
||||
tier: Schema.Struct({
|
||||
type: Schema.Literal("context"),
|
||||
size: Schema.Int,
|
||||
}).pipe(Schema.optional),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
})
|
||||
export type Cost = typeof Cost.Type
|
||||
|
||||
export const Limit = Schema.Struct({
|
||||
context: Schema.Int,
|
||||
input: Schema.Int.pipe(Schema.optional),
|
||||
output: Schema.Int,
|
||||
})
|
||||
export type Limit = typeof Limit.Type
|
||||
|
||||
export const Ref = Schema.Struct({
|
||||
id: ID,
|
||||
providerID: ProviderV2.ID,
|
||||
variant: VariantID.pipe(Schema.optional),
|
||||
})
|
||||
export type Ref = typeof Ref.Type
|
||||
|
||||
export class Info extends Schema.Class<Info>("ModelV2.Info")({
|
||||
id: ID,
|
||||
apiID: ID,
|
||||
providerID: ProviderV2.ID,
|
||||
family: Family.pipe(Schema.optional),
|
||||
name: Schema.String,
|
||||
endpoint: ProviderV2.Endpoint,
|
||||
options: Schema.Struct({
|
||||
...ProviderV2.Options.fields,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
capabilities: Capabilities,
|
||||
variants: Variant.pipe(Schema.Array),
|
||||
time: Schema.Struct({
|
||||
released: DateTimeUtcFromMillis,
|
||||
}),
|
||||
cost: Cost.pipe(Schema.Array),
|
||||
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
||||
enabled: Schema.Boolean,
|
||||
limit: Limit,
|
||||
}) {
|
||||
static empty(providerID: ProviderV2.ID, modelID: ID) {
|
||||
return new Info({
|
||||
id: modelID,
|
||||
apiID: modelID,
|
||||
providerID,
|
||||
name: modelID,
|
||||
endpoint: {
|
||||
type: "unknown",
|
||||
},
|
||||
capabilities: {
|
||||
tools: false,
|
||||
input: [],
|
||||
output: [],
|
||||
},
|
||||
options: {
|
||||
headers: {},
|
||||
body: {},
|
||||
aisdk: { provider: {}, request: {} },
|
||||
},
|
||||
variants: [],
|
||||
time: {
|
||||
released: DateTime.makeUnsafe(0),
|
||||
},
|
||||
cost: [],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: {
|
||||
context: 0,
|
||||
output: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Catalog Interface
|
||||
|
||||
```ts
|
||||
export interface Interface {
|
||||
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||
readonly provider: {
|
||||
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError>
|
||||
readonly all: () => Effect.Effect<ProviderV2.Info[]>
|
||||
readonly available: () => Effect.Effect<ProviderV2.Info[]>
|
||||
}
|
||||
|
||||
readonly model: {
|
||||
readonly get: (
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
|
||||
readonly all: () => Effect.Effect<ModelV2.Info[]>
|
||||
readonly available: () => Effect.Effect<ModelV2.Info[]>
|
||||
readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ProviderV2.Info.enabled` is stored provider state. Provider plugins set it to `false` or record whether availability comes from environment, account, or custom configuration.
|
||||
|
||||
`ProviderV2.Endpoint` includes `{ type: "unknown" }`. `CatalogV2.model.get()` and `CatalogV2.model.all()` resolve `unknown` endpoints from the provider before returning models.
|
||||
|
||||
Model storage is nested by provider because model ids are only unique within a provider.
|
||||
|
||||
```ts
|
||||
type ProviderRecord = {
|
||||
provider: ProviderV2.Info
|
||||
models: HashMap.HashMap<ModelV2.ID, ModelV2.Info>
|
||||
}
|
||||
|
||||
let records = HashMap.empty<ProviderV2.ID, ProviderRecord>()
|
||||
```
|
||||
|
||||
`ModelV2.Info.enabled` stores model availability. `CatalogV2.model.available()` also requires a usable provider.
|
||||
|
||||
```ts
|
||||
const available = provider.enabled !== false && model.enabled
|
||||
```
|
||||
|
||||
## Current Session Runner Adaptation
|
||||
|
||||
The first local V2 Session runner waits for Location plugin boot, then resolves an explicit Session model without silently falling back. Without an explicit model it uses a supported Location catalog default, then falls back to the first available model with a supported route, and otherwise fails with `SessionRunnerModel.ModelNotSelectedError`. Its native adaptation surface is deliberately narrow:
|
||||
|
||||
```text
|
||||
openai/responses over HTTP
|
||||
openai/completions for OpenAI Chat
|
||||
openai/completions for OpenAI-compatible Chat
|
||||
anthropic/messages
|
||||
aisdk:@ai-sdk/openai
|
||||
aisdk:@ai-sdk/openai-compatible with an explicit URL
|
||||
aisdk:@ai-sdk/anthropic
|
||||
```
|
||||
|
||||
Native endpoint URLs are complete endpoint URLs and are split into base URL plus request path when building an LLM route. AI SDK endpoint URLs remain base URLs. The adapter preserves model headers and body options, environment-backed provider credentials, direct model API keys, and selected Session variant overlays.
|
||||
|
||||
Unsupported routes fail explicitly with `SessionRunnerModel.UnsupportedEndpointError`. In particular, `openai/responses` with WebSocket transport must not silently downgrade to HTTP. Google, Azure, Bedrock, OpenRouter-specific behavior, GitHub Copilot, Vertex, gateway adapters, and signed authentication remain future provider slices.
|
||||
|
||||
## Plugin Interface
|
||||
|
||||
```ts
|
||||
type HookSpec = {
|
||||
"account.update": {
|
||||
input: {
|
||||
id: AccountV2.ID
|
||||
serviceID: AccountV2.ServiceID
|
||||
}
|
||||
output: {
|
||||
description: string
|
||||
credential: AccountV2.Credential
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
|
||||
"account.remove": {
|
||||
input: {
|
||||
account: AccountV2.Info
|
||||
}
|
||||
output: {
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
|
||||
"account.activate": {
|
||||
input: {}
|
||||
output: {
|
||||
from?: AccountV2.ID
|
||||
to: AccountV2.ID
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
|
||||
"account.activated": {
|
||||
input: {
|
||||
from?: AccountV2.ID
|
||||
to: AccountV2.ID
|
||||
}
|
||||
output: {}
|
||||
}
|
||||
}
|
||||
|
||||
export type Definition<R = never> = Effect.Effect<
|
||||
{
|
||||
readonly order: number
|
||||
readonly hooks: HookFunctions
|
||||
},
|
||||
never,
|
||||
R
|
||||
>
|
||||
|
||||
export interface Interface {
|
||||
readonly add: <R = never>(input: { id: ID; definition: Definition<R> }) => Effect.Effect<void, never, R>
|
||||
|
||||
readonly remove: (id: ID) => Effect.Effect<void>
|
||||
|
||||
readonly trigger: <Name extends keyof Hooks>(name: Name, input: HookInput<Name>) => Effect.Effect<HookInput<Name>>
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Order
|
||||
|
||||
```ts
|
||||
export const Order = {
|
||||
modelsDev: 0,
|
||||
env: 10,
|
||||
account: 20,
|
||||
provider: 30,
|
||||
config: 40,
|
||||
discovery: 50,
|
||||
} as const
|
||||
```
|
||||
|
||||
## Built-In Plugins
|
||||
|
||||
```ts
|
||||
export const ModelsDevPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.Service | ModelsDev.Service>
|
||||
|
||||
export const EnvPlugin: PluginV2.Definition<ProviderV2.Service | Env.Service>
|
||||
|
||||
export const AccountPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service>
|
||||
|
||||
export const ConfigPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.Service | Config.Service>
|
||||
|
||||
export const AnthropicPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service>
|
||||
|
||||
export const OpenRouterPlugin: PluginV2.Definition<ProviderV2.Service>
|
||||
|
||||
export const AmazonBedrockPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service | Env.Service>
|
||||
|
||||
export const GoogleVertexPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service | Env.Service>
|
||||
|
||||
export const GitLabPlugin: PluginV2.Definition<ProviderV2.Service | AccountV2.Service | Env.Service>
|
||||
|
||||
export const GitLabDiscoveryPlugin: PluginV2.Definition<ProviderV2.Service | ModelV2.Service | AccountV2.Service>
|
||||
```
|
||||
|
||||
## Plugin Hooks
|
||||
|
||||
```ts
|
||||
export type Hooks = {
|
||||
init: {}
|
||||
|
||||
"provider.update": {
|
||||
provider: Draft<ProviderV2.Info>
|
||||
cancel: boolean
|
||||
}
|
||||
|
||||
"model.update": {
|
||||
model: Draft<ModelV2.Info>
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
# Policy
|
||||
# Provider Policy Proposal
|
||||
|
||||
Status: **Proposed and unimplemented.** This document is design input, not current configuration or runtime behavior.
|
||||
|
||||
## Purpose
|
||||
|
||||
Policies control whether an operation on a named resource is allowed. They may be authored in configuration files, but policy evaluation is its own runtime concern.
|
||||
This proposal would let policies control whether an operation on a named resource is allowed. Statements could be authored in configuration files while policy evaluation remained its own runtime concern.
|
||||
|
||||
The first policy consumer is provider availability:
|
||||
|
||||
|
|
@ -58,7 +60,7 @@ interface PolicyInfo {
|
|||
}
|
||||
```
|
||||
|
||||
The `Policy` module owns the shared `Policy.Info` interface, `Policy.Effect` type, and evaluator. Domains define their supported typed statement schemas; for example, `Catalog.ProviderPolicy` fixes `action` to `"provider.use"`. The config schema gathers those domain-defined statement schemas into the accepted `experimental.policies` union because config files are one place statements can be authored while the capability is experimental.
|
||||
The proposed `Policy` module would own the shared `Policy.Info` interface, `Policy.Effect` type, and evaluator. Domains would define their supported typed statement schemas; for example, `Catalog.ProviderPolicy` would fix `action` to `"provider.use"`. The config schema could gather those domain-defined statement schemas into an `experimental.policies` union.
|
||||
|
||||
## Matching
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# V2 Schema Changelog
|
||||
|
||||
Status: **Historical pre-release compatibility ledger.** Older entries retain the names and behavior that were accurate when written; current contracts live in Protocol, Schema, Core, and the indexed specifications.
|
||||
|
||||
## 2026-07-09: Make Session Input Storage Pending-Only And Rename It To Session Pending
|
||||
|
||||
- Rename the `SessionInput` schema namespace to `SessionPending` and the `session_input` table to `session_pending`.
|
||||
|
|
@ -147,9 +149,7 @@ Compatibility:
|
|||
- Preserve full durable history; compaction changes only the active model representation.
|
||||
- Defer provider-overflow recovery, explicit manual compaction, and deterministic old tool-result pruning.
|
||||
|
||||
Record V2 database, durable-event, projected-message, HTTP, and generated SDK schema changes here. Each entry states why the contract changed and whether consumers or stored data need compatibility handling. Commit messages for schema-affecting changes should include the same summary.
|
||||
|
||||
This document covers meaningful contract changes introduced on the `feat/opencode-embedded-api` branch since its divergence from `origin/dev`. Mechanical file moves and internal refactors are omitted unless they changed stored data, replay behavior, public HTTP or SDK shapes, or model-facing tool contracts.
|
||||
The entries below record meaningful contract changes from the pre-release V2 rebuild. Mechanical file moves and internal refactors were omitted unless they changed stored data, replay behavior, public HTTP or SDK shapes, or model-facing tool contracts.
|
||||
|
||||
## 2026-06-04 Event-Sourced Session Input Cutover
|
||||
|
||||
|
|
@ -905,7 +905,7 @@ Change:
|
|||
- Request Context Epoch replacement after an agent switch, dynamically re-observe the effective agent during retries, and fence first-epoch creation against the authoritative effective agent.
|
||||
- Fence existing-epoch replacement against the authoritative effective agent and block cross-agent provider turns while replacement context is unavailable.
|
||||
- Group the System Context algebra, registry, and built-ins under `system-context/`; keep source producers and Context Epoch persistence with their owning Skill, instruction, and Session modules; rename projected conversation selection to Session History.
|
||||
- Add the canonical V1-to-V2 runtime-context parity checklist to `specs/v2/session.md`.
|
||||
- Add the then-current V1-to-V2 runtime-context parity checklist to `specs/v2/session.md` (later removed with the completed migration plan).
|
||||
|
||||
Compatibility:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
# RFC: Continue Sessions After Managed-Service Restart
|
||||
# Decision: Continue Sessions After Managed-Service Restart
|
||||
|
||||
| Field | Value |
|
||||
| -------------- | ------------------------------------------------------------ |
|
||||
| Status | Proposed |
|
||||
| Status | Accepted and implemented |
|
||||
| Author | Kit Langton |
|
||||
| Date | 2026-07-08 |
|
||||
| Tracking issue | [#35646](https://github.com/anomalyco/opencode/issues/35646) |
|
||||
|
||||
## Summary
|
||||
|
||||
When the managed OpenCode server shuts down gracefully, active Sessions should continue automatically the next time the managed server starts.
|
||||
When the managed OpenCode server shuts down gracefully, active Sessions continue automatically the next time the managed server starts.
|
||||
|
||||
This RFC proposes one private nullable timestamp on the existing Session row: `time_suspended`. The managed server suspends its active Sessions on graceful shutdown and resumes suspended Sessions on startup. Both are explicit actions the managed server invokes; no other server ever suspends or auto-resumes.
|
||||
The implementation uses one private nullable timestamp on the existing Session row: `time_suspended`. The managed server suspends its active Sessions on graceful shutdown and resumes suspended Sessions on startup. Both are explicit actions the managed server invokes.
|
||||
|
||||
The field is not Session status. Live activity remains process-local. Hard-crash recovery and exactly-once provider or tool execution remain out of scope.
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ WHERE id = ? AND time_suspended IS NOT NULL
|
|||
RETURNING id;
|
||||
```
|
||||
|
||||
Only the process receiving the returned row resumes that Session. A second consumer receives no row. Each suspension is consumed right before its own drain starts, and at most a handful of resumed drains run at once.
|
||||
Only the process receiving the returned row resumes that Session. A second consumer receives no row. Each suspension is consumed right before its own drain starts, and at most four resumed drains run at once.
|
||||
|
||||
The resume goes through the existing process-local coordinator, which joins duplicate same-process resumes and starts a forced drain while idle.
|
||||
|
||||
|
|
@ -102,14 +102,14 @@ The resume goes through the existing process-local coordinator, which joins dupl
|
|||
|
||||
The design provides at-most-once automatic scheduling, not guaranteed continuation.
|
||||
|
||||
| Failure | Result |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
||||
| Old server is killed before graceful closeout | Nothing is suspended; user resumes manually |
|
||||
| Old server dies between suspension and teardown | Session stays suspended; next server resumes it |
|
||||
| New server crashes before conditional clear | Session stays suspended |
|
||||
| New server crashes after clear but before drain starts | Automatic continuation is lost |
|
||||
| New server crashes after drain starts | No automatic hard-crash retry |
|
||||
| Interrupted tool has uncertain side effects | Existing stale-tool reconciliation records failure rather than replaying the old call |
|
||||
| Failure | Result |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------------------------------- |
|
||||
| Old server is killed before graceful closeout | Nothing is suspended; user resumes manually |
|
||||
| Old server dies between suspension and teardown | Session stays suspended; next server resumes it |
|
||||
| New server crashes before conditional clear | Session stays suspended |
|
||||
| New server crashes after clear but before drain starts | Automatic continuation is lost |
|
||||
| New server crashes after drain starts | No automatic hard-crash retry |
|
||||
| Interrupted tool has uncertain side effects | Orphan reconciliation records interruption rather than replaying the old call |
|
||||
|
||||
Losing one automatic continuation is safer than repeatedly restarting ambiguous provider or tool work.
|
||||
|
||||
|
|
@ -130,16 +130,15 @@ An old shutdown event records what happened; it does not prove that a future pro
|
|||
| 5 | Persisted general Session status | Reject | Conflates activity, history, and pending work |
|
||||
| 6 | Scan lifecycle history on every startup | Reject | Repeats unbounded historical work and lacks direct atomic consumption |
|
||||
|
||||
## Verification
|
||||
## Coverage
|
||||
|
||||
Required regression coverage:
|
||||
Regression coverage verifies:
|
||||
|
||||
- A suspension can be consumed only once per Session.
|
||||
- Generic lifecycle publication and replay do not infer suspension.
|
||||
- Historical shutdown events remain unsuspended after migration.
|
||||
- Concurrent managed-service candidates elect one process and produce one continued execution.
|
||||
- Teardown interruption preserves suspension; a drain finishing on its own clears it.
|
||||
- Only the managed server suspends and resumes; default, embedded, and stdio servers never invoke the actions.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
|
|
|
|||
|
|
@ -1,260 +1,90 @@
|
|||
# Session API
|
||||
# V2 Session Contract
|
||||
|
||||
## Current V2 Core Slice
|
||||
Status: **Current semantic overview.** Protocol owns public operations, Schema owns public shapes and durable events, and Core owns execution and persistence behavior. [CONTEXT.md](../../CONTEXT.md) defines the canonical terms used here.
|
||||
|
||||
The Effect-native core facade treats prompt recording and execution as separate responsibilities:
|
||||
## Prompt Admission Precedes Execution
|
||||
|
||||
```text
|
||||
sessions.create({ id?, location, ... })
|
||||
-> omitted ID generates one internal Session ID
|
||||
-> supplied ID creates the Session when absent
|
||||
-> reused ID returns the existing Session identity
|
||||
|
||||
sessions.prompt({ id?, sessionID, prompt, delivery?, resume? })
|
||||
-> omitted ID generates one internal message ID
|
||||
-> supplied ID inserts one durable Session inbox row when absent
|
||||
-> exact reuse returns the same admission receipt
|
||||
-> reusing one message ID for another Session, prompt, or delivery mode fails
|
||||
-> exact retry schedules another wake unless resume is false
|
||||
-> resume omitted or true schedules execution after admission
|
||||
-> resume false admits only
|
||||
`SessionV2.prompt(...)` records one durable `session.input.admitted` fact and one `session_pending` row before advisory execution begins. Pending input remains outside model-visible Session History until promotion. The promotion transaction publishes `session.input.promoted`, projects the visible message, and consumes the pending row atomically.
|
||||
|
||||
sessions.interrupt(sessionID)
|
||||
-> interrupts active execution on this process
|
||||
-> waits for runner cleanup and a terminal lifecycle observation
|
||||
-> clears a coalesced follow-up wake already registered with this coordinator
|
||||
-> preserves durable inbox rows for a later wake or resume
|
||||
-> idle or missing Session is a no-op
|
||||
Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. A retry of an already-promoted input reconciles against projected history and its durable admission event.
|
||||
|
||||
sessions.active()
|
||||
-> snapshots foreground Session drains owned by this process
|
||||
-> returns only active Session IDs with { type: "running" }
|
||||
-> absence means inactive; activity is not durable across process restarts
|
||||
```
|
||||
`resume` controls scheduling, not durability:
|
||||
|
||||
`session_input` is the typed durable admission inbox for prompts and Session control operations. `PromptAdmitted` records accepted user input; `Compaction.Admitted` records one coalesced manual compaction barrier. Admitted prompts remain outside model-visible Session history until the serialized runner publishes `Prompted`. Its projector atomically writes the visible user message and marks the inbox row promoted in the same event transaction. A pending compaction blocks all unpromoted prompts, runs before the Session would otherwise become idle, and releases the backlog only after its durable ended or failed event settles the barrier. The V1-to-V2 shadow bridge publishes the same `Prompted` event for already-visible V1 prompts.
|
||||
- Omitted or `true` records the input, then schedules `SessionExecution.wake(sessionID)`.
|
||||
- `false` records the input without scheduling execution.
|
||||
|
||||
`admittedSeq` is the durable Session event sequence of `PromptAdmitted`. Clients may use the admission event to represent queued input before `Prompted` makes it part of visible conversation history.
|
||||
Delivery is explicit:
|
||||
|
||||
Execution routing starts from only the Session ID:
|
||||
- `steer` is the default. Steers promote together at the next Safe Step Boundary while the current Session Drain still requires continuation.
|
||||
- `queue` remains pending while the Session can continue. When the Session would otherwise become idle, one queued input promotes; the runner then reevaluates continuation before promoting another.
|
||||
|
||||
```text
|
||||
SessionExecution.resume(sessionID)
|
||||
-> SessionStore.get(sessionID)
|
||||
-> LocationServiceMap.get(session.location)
|
||||
-> SessionRunner.drain({ sessionID, force? })
|
||||
```
|
||||
Promoting new user input resets the selected agent's step allowance. A batch of steers resets it once.
|
||||
|
||||
`SessionExecution` and the read-side `SessionStore` are process-global. `SessionRunner`, catalog, model resolver, tool registry, permission state, and filesystem are cached per Location. No layer takes a Session ID. An omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
|
||||
Manual compaction uses the same pending store as one coalesced barrier. The barrier blocks later input promotion until compaction ends or fails, then is consumed.
|
||||
|
||||
The local runner issues one explicit `llm.stream(request)` per step, projects each complete local tool call durably before eagerly starting its structured child execution, awaits every owned tool fiber after provider-stream closure, and reloads projected history once before continuation. For every in-process step, `session.step.started` precedes its tool calls, every local and hosted call settles as `session.tool.success` or `session.tool.failed`, and only then may the runner publish the single terminal `session.step.ended` or `session.step.failed`. Streamed provider-error evidence is retained until this closeout; thrown provider failures and interruption use the same settlement-first ordering. Promoting any new user input resets the selected agent's configured step allowance; multiple steers promoted at one boundary reset it once.
|
||||
## Execution Is Process-Local
|
||||
|
||||
`callID` is unique only within its owning step, not across the Session. Tool events therefore carry `assistantMessageID`, and consumers correlate a call through the step that owns that assistant message rather than inventing a synthetic composite key. Before assembling a provider request, the runner's cross-drain `failInterruptedTools` recovery durably fails any tool still projected as pending or running from a previous process with `Tool execution interrupted`. This orphan-recovery sweep is the explicit nesting exception: it occurs in a later drain, but attributes every settlement to the original `assistantMessageID`; abandoned side effects are never silently replayed.
|
||||
`SessionExecution` is process-global and keyed only by Session ID. At drain start it loads the Session, enters its Location through `LocationServiceMap`, and invokes the Location-scoped runner. The runner, model resolution, tools, permissions, plugins, and filesystem remain Location-scoped.
|
||||
|
||||
`session.execution.started.1` and exactly one of `session.execution.succeeded.1`, `session.execution.failed.1`, or `session.execution.interrupted.1` observe one process-local coordinator busy period, including coalesced drains and joined resumes. These durable rows are history, not a durable execution identity: replay must never infer current liveness, recovery, grouping, or resumability from an unmatched start. A drain has no durable identity or transcript boundary. `/api/session/active` is the authority for current process-local liveness, and is empty after restart. User interruption records `reason: "user"`; owner-scope interruption defaults to `"shutdown"`; `"superseded"` is reserved for explicit replacement.
|
||||
`SessionRunCoordinator` provides the local ownership rules:
|
||||
|
||||
Core retries only typed rate-limit, provider-internal, and transport failures before durable assistant text, reasoning, tool-call, tool-output, or tool-execution evidence. The initial call plus at most four retries use two-second exponential backoff, raised when a provider's `retryAfterMs` is larger. Every retry attempt remains a distinct step and consumes the selected agent's step allowance, while all pre-output attempts reuse one assistant message ID so retry state never creates empty transcript messages. Repeated `session.step.started.1` facts reopen that assistant projection idempotently. `session.retry.scheduled.1` is committed before each delay with the upcoming one-based attempt and absolute epoch-millisecond time, then projects onto `Assistant.retry`. The next `session.step.started.1` or terminal failure/interruption clears it. A scheduled retry surviving a crash is historical UI state only and never triggers recovery.
|
||||
- Explicit resumes join the active execution for the same Session.
|
||||
- Repeated wakes coalesce into one follow-up drain.
|
||||
- Different Sessions run concurrently.
|
||||
- Interruption stops locally owned execution without deleting pending input.
|
||||
|
||||
A normalized `step-finish` with `content-filter` publishes `session.step.failed.1` with `provider.content-filter`, never `session.step.ended.1`. Any partial streamed content remains visible; a contentless filtered response still has a failed assistant projection.
|
||||
The public interrupt operation verifies that the durable Session exists. An unknown Session fails with `SessionNotFoundError`; a known Session that is idle, settled, or not locally owned is a no-op.
|
||||
|
||||
Projected hosted tools preserve call-side and settlement-side provider metadata separately so settlement and interruption recovery cannot erase continuation identifiers. Provider-native reasoning and provider metadata replay only while the historical assistant model matches the selected continuation model; after a model switch, visible reasoning text remains ordinary assistant text and provider-native metadata is omitted.
|
||||
`sessions.active()` snapshots foreground drains currently owned by this process. Durable execution events are historical observations, not liveness or ownership records.
|
||||
|
||||
## Instruction Checkpoints
|
||||
The managed server provides graceful restart continuity through private Session suspension. Shutdown marks active Sessions before interrupting them; the next managed server atomically consumes each suspension and schedules at most one resume. Hard-crash recovery and exactly-once provider or tool execution remain out of scope. See [Managed restart continuation](./session-restart-continuation.md).
|
||||
|
||||
V2 Sessions persist the exact privileged instructions shown to the model. `InstructionCheckpoint` stores one immutable instruction baseline, its baseline event sequence, and a model-hidden `Instructions.Applied` record used to compare independently observed instruction sources. Instructions are only one part of Model Context: the runner separately assembles agent or provider system text, Session History, tool definitions, and step-local additions for each request.
|
||||
## One Step Owns One Logical LLM Call
|
||||
|
||||
The runner has no instruction registry. `loadInstructions` explicitly loads these producers concurrently and combines them in this fixed order:
|
||||
Before each Step, the runner reloads Session History, resolves the selected agent and model, prepares instructions, and materializes tools. Most Steps make one Physical Attempt; overflow-triggered compaction recovery may rebuild the same Step for one additional provider request.
|
||||
|
||||
1. Instruction built-ins, currently environment facts and the host-local date.
|
||||
2. `InstructionDiscovery`, observing ambient `AGENTS.md` files.
|
||||
3. Selected-agent available-skill guidance.
|
||||
4. Reference guidance.
|
||||
5. Selected-agent MCP guidance.
|
||||
6. API-managed `InstructionEntry` values for the Session.
|
||||
Each complete local tool call is durable before side effects begin. Local calls start eagerly and may run concurrently, but settlement publication remains serialized. Every local and hosted call reaches durable success or failure before the Step publishes its single terminal ended or failed event.
|
||||
|
||||
`Instructions.combine(...)` preserves that caller order and rejects duplicate namespaced source keys. Each source owns its typed observation, JSON codec, and pure baseline, update, and optional removal renderers.
|
||||
Tool calls belong to their assistant message. `callID` is unique only within that Step, so durable tool events also carry `assistantMessageID`.
|
||||
|
||||
The first complete observation initializes `InstructionCheckpoint` before any pending prompt becomes model-visible. If an initial source is temporarily unavailable, execution stops while the prompt remains pending and retryable. Every later step attempt also prepares instructions before input promotion. Changed instructions publish one durable chronological System message through `session.instructions.updated`, and that event commit advances `Instructions.Applied` atomically.
|
||||
Before `runStep` assembles its provider request, orphan reconciliation fails tool calls still projected as streaming or running from an earlier process. It preserves the original assistant attribution and never replays ambiguous side effects.
|
||||
|
||||
```text
|
||||
Client Runner Explicit producers InstructionCheckpoint Inbox / History LLM
|
||||
│ │ │ │ │ │
|
||||
├─ Admit prompt ────────────────────────────────────────────────────────────────────────────────▶ │
|
||||
│ │ │ │ │ │
|
||||
│ ├─ Load instructions ───────▶ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ ◀─ Combined sources ─────────┤ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ ├─ Initialize or reconcile ────────────────────────────▶ │ │
|
||||
│ │ │ │ │ │
|
||||
│ ├─ Publish update + advance Applied atomically ───────────────────────────────▶ │
|
||||
│ │ │ │ │ │
|
||||
│ ├─ Promote eligible input ────────────────────────────────────────────────────▶ │
|
||||
│ │ │ │ │ │
|
||||
│ ├─ System text + instruction baseline + history + tools ──────────────────────────────────────▶
|
||||
```
|
||||
After local settlement, continuation reloads projected history and begins a new Step. The runner never delegates orchestration to an in-memory tool loop.
|
||||
|
||||
Agent and model selection are step-scoped. The runner selects the agent before loading agent-specific guidance; a switch admitted after the current boundary applies to the next step without restarting the current one. Changed guidance is admitted through `session.instructions.updated` while preserving the baseline. Model selection affects Model Context assembly but is not an instruction source and does not itself replace the instruction baseline.
|
||||
## Retry Is Narrow And Observable
|
||||
|
||||
A completed compaction causes the next physical attempt to rebaseline from current instructions. Temporarily unavailable sources are restated from the model's last applied belief where possible. A Session move resets `InstructionCheckpoint` so the destination Location initializes a complete baseline on its next run. Committed revert also resets the checkpoint.
|
||||
Core retries typed rate-limit, provider-internal, and transport failures only before durable assistant content, tool-call, tool-output, or tool-execution evidence exists. The initial request plus at most four retries use exponential backoff, increased when the provider supplies a longer retry delay.
|
||||
|
||||
```text
|
||||
Session InstructionCheckpoint
|
||||
│ │
|
||||
├─ initialize complete baseline ────▶
|
||||
│ │
|
||||
│ ├──────────────────────────────╮
|
||||
│ │ reconcile instruction update │
|
||||
│ ◀──────────────────────────────╯
|
||||
│ │
|
||||
├─ completed compaction ────────────▶ rebaseline
|
||||
│ │
|
||||
├─ move or committed revert ────────▶ reset
|
||||
```
|
||||
Each retry attempt is a distinct Step, consumes the selected agent's allowance, and reuses the assistant message ID while no durable output exists. `session.retry.scheduled` records the next attempt and absolute retry time. A later Step start or terminal failure clears projected retry state. Surviving retry history never triggers post-crash recovery by itself.
|
||||
|
||||
`InstructionDiscovery` observes ambient instructions as one ordered aggregate source. Ambient discovery canonicalizes traversal within the project root, reads global and upward-project `AGENTS.md` files, and honors `OPENCODE_DISABLE_PROJECT_CONFIG` for project files.
|
||||
A normalized content-filter finish fails the Step. Any partial streamed content remains visible.
|
||||
|
||||
An unavailable observation preserves the previously applied value. A confirmed partial instruction removal emits the complete remaining aggregate with explicit supersession text; removing the final instruction emits a revocation message.
|
||||
## Instructions Are Session-Owned
|
||||
|
||||
Current instruction follow-ups:
|
||||
`InstructionCheckpoint` stores the exact instruction baseline last shown to the model and an `Instructions.Applied` record for each source. The runner explicitly combines built-ins, ambient discovery, selected-agent skill guidance, references, MCP guidance, and API-managed instruction entries. There is no instruction registry.
|
||||
|
||||
- Add configured and remote instruction sources with explicit precedence and removal semantics.
|
||||
- Add durable post-crash continuation recovery for promoted or provider-dispatched work.
|
||||
- Add operational metrics for observation latency, unavailable sources, contention, baseline size, and chronological-update growth.
|
||||
- Consider watcher-backed per-file caching only if measurements show direct step-boundary observation is too expensive.
|
||||
- Design any plugin-defined instruction contribution as an explicit runner composition boundary; do not reintroduce a registry implicitly.
|
||||
- Add clustered Session execution ownership and stale-runtime fencing.
|
||||
The first complete observation establishes the baseline before input promotion. Later changes publish chronological `session.instructions.updated` messages and advance applied state atomically. An unavailable source preserves the model's previous belief; it blocks only a Session that has never established a complete baseline.
|
||||
|
||||
## Automatic Compaction
|
||||
Completed compaction rebaselines instructions. Session movement and committed revert reset the checkpoint. Model selection affects request assembly but is not itself an instruction source.
|
||||
|
||||
Before each step, the runner estimates the complete model-visible request and compares it with the selected model's context window minus absolute reserved headroom. The reserve is the greater of the requested/model output allowance and configured `compaction.buffer`. When the request exceeds that budget and older complete steps are available, the runner compacts before executing the pending step.
|
||||
## Compaction Rebuilds Active History
|
||||
|
||||
Compaction keeps the full transcript durable while replacing its active model representation with one hidden checkpoint containing a structured rolling summary and token-bounded serialized recent context. Provider-native assistant, reasoning, and tool messages never survive across the boundary, avoiding signature and encrypted-reasoning failures when the earlier prefix changes.
|
||||
Before each Step, the runner estimates the complete model-visible request against the selected model's context window and reserved output headroom. When compaction is enabled, model limits are known, and enough older Session History is available, the runner may store a structured rolling summary plus bounded recent context instead of sending an over-budget request.
|
||||
|
||||
The rolling summary is a continuation checkpoint with this complete heading order: `Objective`, `Important Details`, `Work State`, and `Next Move`. `Work State` records completed, active, and blocked work, while `Next Move` records the immediate and following actions. Every heading remains present even when its value is `(none)`.
|
||||
The full transcript remains durable. Active model history after the checkpoint contains the summary and retained recent context; provider-native continuation state does not cross the compaction boundary.
|
||||
|
||||
`session.compaction.admitted.1` durably records a manual request and projects its queued transcript row. `session.compaction.started.1` identifies the attempt and transforms that row into a running divider. Compaction deltas are live-only progress rendered beneath it. `session.compaction.ended.1` durably stores the final summary and serialized recent context, completes the same row, and settles the manual barrier. `session.compaction.failed.1` settles an unsuccessful manual barrier without changing the previous history boundary. On the next physical attempt, the runner observes a completed compaction and directly renders a fresh instruction baseline through `InstructionCheckpoint`.
|
||||
If the provider reports context overflow before durable assistant output or tool execution, the runner may perform one overflow-triggered compaction and rebuild the same logical Step. A second overflow or any overflow after durable output is terminal.
|
||||
|
||||
Assistant text and reasoning follow a strict `started` / live-only `delta` / durable full-value `ended` lifecycle. A publisher permits at most one open fragment of each kind in a step and fails on a second start before the matching end. Provider block IDs remain internal to LLM adapters; each fragment event carries a Session-assigned kind-specific ordinal, matching the ordinal derived from projected content. UI identity is therefore the assistant message ID plus content kind and ordinal. Tool calls retain step-scoped `callID` because settlements and provider replay correlate through it.
|
||||
## Durable Events Are Session-Scoped
|
||||
|
||||
Provider continuation state is opaque and un-nested at the Session boundary. The publisher selects only the active model provider's entry from LLM provider metadata. Same-model replay re-nests that state under the current provider; model switches and failed assistant steps continue to suppress provider-native continuation state.
|
||||
`sessions.log({ sessionID, after?, follow? })` verifies the Session and reads public durable Session events after an exclusive aggregate sequence. With `follow: true`, it subscribes before replay and emits one synchronization marker at the captured watermark before live durable events continue.
|
||||
|
||||
Repeated compactions update the previous structured summary with newly compacted messages. The runner then reloads projected history and executes the original pending step.
|
||||
Live-only text, reasoning, tool-input, and compaction deltas are intentionally absent from replay. The instance-wide live event stream has different schemas and no replay guarantee.
|
||||
|
||||
When a provider rejects a request as context overflow before durable assistant output or tool execution, the runner attempts one overflow-triggered compaction even when the local estimate did not predict pressure. A completed checkpoint rebuilds the same logical step with one remaining physical attempt. A second overflow, unavailable compaction, or overflow after durable output becomes the ordinary terminal failure; recovery never loops or replays partial side effects. Deterministic old tool-result pruning remains a separate follow-up.
|
||||
There is no separate finite Session-history endpoint. Request/response consumers use authoritative Session projections such as messages, pending input, and context; replay consumers use the durable log.
|
||||
|
||||
## V1 Model Context Parity
|
||||
## Recovery Boundaries Stay Explicit
|
||||
|
||||
This is the canonical checklist for Model Context still needed before the V2 runner replaces V1. Keep each behavior in its owning boundary rather than treating all model-visible text as durable Instructions. Update this table in the PR that changes a status.
|
||||
An advisory wake does not infer that ambiguous provider work is safe to retry after input promotion. Explicit resume may continue from durable projected history, but automatic hard-crash continuation requires a separate design covering provider-dispatch ambiguity, tool idempotency, retry budgets, and future clustered ownership.
|
||||
|
||||
Status: `complete` is usable in the native V2 path, `partial` covers only part of V1 behavior, and `missing` has no native V2 equivalent.
|
||||
|
||||
| Boundary | Behavior | Status | Remaining V2 work |
|
||||
| -------------------------- | ------------------------------------------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Durable Instruction Source | Environment facts and host-local date | partial | Keep selected provider/model identity in step request assembly rather than a stale Location-wide instruction value. |
|
||||
| Durable Instruction Source | Global and upward project instructions | partial | Decide whether V2 also discovers legacy `CLAUDE.md` and deprecated `CONTEXT.md`. |
|
||||
| Durable Instruction Source | Configured local/glob and remote URL instructions | missing | Add independent sources with explicit precedence, unavailable, and removal semantics. |
|
||||
| Durable Instruction Source | Nearby nested instructions discovered after successful reads | missing | Persist discoveries and admit them at the next safe step boundary. |
|
||||
| Durable Instruction Source | Selected-agent available skill guidance and skill-body loading | partial | Guidance and body exposure are permission-filtered; remove globally denied skill definitions during request-time tool materialization. |
|
||||
| Step request assembly | Placement, selected model, chronological history, and canonical lowering | complete | None. |
|
||||
| Step request assembly | Selected agent, agent prompt, and effective permissions | partial | V2 uses selected-agent permissions for skill guidance and tool authorization; still apply the agent system prompt and request policy. |
|
||||
| Step request assembly | Provider/model-specific base instructions | complete | Native V2 selects the provider-family baseline unless the effective agent overrides it. |
|
||||
| Step request assembly | Policy-filtered built-in, MCP, plugin, and structured-output tools | partial | Materialize definitions for the effective agent and request. |
|
||||
| Step request assembly | Per-prompt system text and tool overrides | missing | Design admission and durable replay semantics before exposing them. |
|
||||
| Step request assembly | Steering, plan/build-switch, and final-step reminders | missing | Add only reminders whose behavior remains part of V2. |
|
||||
| Step request assembly | Plugin message, system, parameter, and header transforms | missing | Design V2 plugin hooks and lifecycle semantics. |
|
||||
| Step request assembly | Model variants and request settings | partial | Apply effective agent options and future plugin-mutated request settings. |
|
||||
| Step request assembly | Structured-output policy | missing | Add prompt format, generated tool, tool choice, and model-visible policy together. |
|
||||
| Step request assembly | Automatic/context-pressure compaction | complete | V2 initiates automatic and overflow-triggered compaction, then rebuilds the baseline from the completed checkpoint. |
|
||||
| Prompt/reference expansion | Durable typed prompt attachments | complete | None. |
|
||||
| Prompt/reference expansion | Native template and `@` mention expansion | missing | Parse and resolve native V2 prompt input before durable admission. |
|
||||
| Prompt/reference expansion | File, directory, media, and MCP-resource materialization | partial | Materialize and normalize sources instead of lowering unresolved attachment metadata. |
|
||||
| Prompt/reference expansion | Agent-reference expansion | missing | Produce permission-aware model-visible task guidance. |
|
||||
| Prompt/reference expansion | Configured-reference expansion | missing | Resolve aliases and emit durable model-visible reference context or failures. |
|
||||
| Prompt/reference expansion | Native synthetic expansion replay | partial | V2 replays synthetic messages but only the V1 compatibility path creates them. |
|
||||
|
||||
Provider timeout and watchdog policy is intentionally deferred. Retry tuning beyond the narrow safe policy above remains separate work; the runner does not impose a universal provider-stream inactivity or absolute timeout.
|
||||
|
||||
Inbox delivery is explicit:
|
||||
|
||||
- `steer` inputs promote at the next safe step boundary, including continuation inside the current drain.
|
||||
- `queue` inputs remain in a FIFO while the current drain requires continuation. When the Session would otherwise become idle, the runner promotes exactly one queued input, then reevaluates continuation before promoting another.
|
||||
|
||||
Execution has two entry points:
|
||||
|
||||
- `run` is an explicit resume. It joins any active execution or starts a forced drain while idle. A forced drain bypasses the no-eligible-input guard, but preparation may still fail before a physical attempt.
|
||||
- `wake` reports newly recorded durable inbox work. Repeated wakes coalesce. A wake calls the provider only when it can promote eligible input.
|
||||
|
||||
Post-crash continuation recovery is intentionally deferred. A wake does not infer that ambiguous provider work is safe to retry after an input has already been promoted. Explicit `run` may deliberately continue from durable projected history. A future recovery slice should model provider-dispatch ambiguity, required continuation, queued-input promotion, retry policy, and visible recovery status together. It must not assume an enclosing durable execution identity that the Session model does not otherwise need.
|
||||
|
||||
A process-global `SessionRunCoordinator` serializes execution for each local Session while allowing different Sessions to run concurrently. Resumes join active execution, overlapping wakes coalesce into one follow-up, and interruption stops current process-local execution without deleting durable inbox work. The runner enters the Session's current Location when execution starts and fences each new step against that Location. Its durable lifecycle events are historical observations only; they do not replace the coordinator's process-local active registry.
|
||||
|
||||
The coordinator's active registry is also the source for `sessions.active()`. It represents only foreground Session drains owned by the current process; background subagents and tasks do not add parent Sessions to this registry. The snapshot is runtime state and is empty after a process restart.
|
||||
|
||||
Inbox promotion coalesces pending steers in durable admission order. Once continuation would otherwise end, it promotes one queued input at a time in FIFO order. Add explicit inbox backlog and steering-batch limits before exposing broad multi-caller admission or untrusted queue growth.
|
||||
|
||||
Eager local-tool execution is intentionally unbounded in the current local slice. This minimizes tool latency but does not increase SQLite settlement throughput: Session-event publication remains serialized per step. Before broadening exposure, revisit per-step call limits, output truncation, and operational backpressure using observed workloads. The normalized Session event schemas remain experimental and unshipped; databases created by earlier experimental builds are disposable rather than compatibility targets.
|
||||
|
||||
The normalized Session event family and projected Session-message model predate this branch. This slice refines their replay contract: projected Session messages retain their source aggregate sequence so canonical context ordering and `sessions.messages(...)` pagination follow durable event order even when caller-supplied IDs differ; event time is carried by the envelope `created` field rather than duplicated in payloads. Consumers can use `sessions.log({ sessionID, after? })` to replay durable Session events after an aggregate sequence cursor, then tail durable events without a race. Live-only text, reasoning, and tool-input fragments remain available through EventV2 subscriptions for connected renderers; they are intentionally absent from the replayable Session stream.
|
||||
|
||||
The first `sessions.log(...)` contract is durable-only during both replay and live tailing. This keeps one cursor equal to one persisted aggregate sequence and is sufficient for reconnect-safe consumers. A later UI-facing API may optionally interleave live-only deltas while connected, but those fragments must remain explicitly ephemeral: they cannot advance the durable cursor, replay after reconnect, or be mistaken for publication boundaries.
|
||||
|
||||
`sessions.log(...)` captures the aggregate's durable watermark before replay and emits one `log.synced` marker when replay reaches that watermark. The marker carries `seq` when the captured watermark is non-empty and omits it when the log has no sequence. With `follow=true`, the tail subscribes before the replay watermark is captured, so events committed during replay are delivered after `log.synced` with a sequence greater than the marker watermark. Durable reads are paged internally; consumers observe only the ordered event stream plus the single marker.
|
||||
|
||||
`sessions.history({ sessionID, after?, limit? })` is the finite counterpart for request/response consumers. `after` is an exclusive aggregate sequence, and omission starts before sequence zero. The response is `{ data, hasMore }`; callers derive the next `after` from the final event's durable sequence when `hasMore` is true. Public durable Session events are selected before pagination, which permits gaps from private or historical aggregate events while preserving strictly increasing unique sequences. The log has a moving head, so events committed between pages may appear on the next page.
|
||||
|
||||
The finite endpoint is `GET /api/session/:sessionID/history`, uses the normal Session Location and authorization middleware, defaults to 50 events, and accepts at most 100. It returns only events in the public durable Session schema. The existing `sessions.log()` replay-and-tail stream is unchanged except for its explicit `log.synced` replay marker.
|
||||
|
||||
`sessions.history({ sessionID, after?, limit? })` is the finite counterpart for request/response consumers. `after` is an exclusive aggregate sequence, and omission starts before sequence zero. The response is `{ data, hasMore }`; callers derive the next `after` from the final event's durable sequence when `hasMore` is true. Public durable Session events are selected before pagination, which permits gaps from private or historical aggregate events while preserving strictly increasing unique sequences. The log has a moving head, so events committed between pages may appear on the next page.
|
||||
|
||||
The finite endpoint is `GET /api/session/:sessionID/history`, uses the normal Session Location and authorization middleware, defaults to 50 events, and accepts at most 100. It returns only events in the public durable Session schema. The existing `sessions.events()` replay-and-tail stream is unchanged.
|
||||
|
||||
Durable event tail wakeups are advisory and edge-triggered. Each active tail owns one sliding-capacity-1 dirty signal for its aggregate and re-queries SQLite after a wake. Repeated commits coalesce while the tail is busy because durable rows, not in-memory notifications, preserve every event and sequence. Subscribe and register the dirty signal before historical replay, then remove it when the tail closes, so replay handoff cannot miss a commit and inactive aggregates retain no wake state.
|
||||
|
||||
Event replay owner claims are separate from clustered Session execution ownership. The former already fences synchronized projection reconstruction; the latter still needs distributed active-run acquisition, stale-runtime rejection, interruption, and placement orchestration.
|
||||
|
||||
## Current Tool Registry Slice
|
||||
|
||||
The Session runner materializes Location tools for each model request and persists their settlements. Registration, authorization, execution, and failure semantics are canonical in [Tools](./tools.md).
|
||||
|
||||
When a Session omits `agent`, both execution and permission evaluation use the default `build` agent. A caller must not observe `build` model behavior while permission checks silently evaluate an empty no-agent policy.
|
||||
|
||||
The first built-in contribution is bounded `read`:
|
||||
|
||||
```text
|
||||
resolve one path relative to the Location or a named project reference
|
||||
-> reject absolute paths, path escapes, and symlink escapes
|
||||
-> authorize read against the canonical resource identity
|
||||
-> for a file: return UTF-8 text or base64 binary content; page oversized UTF-8 text by bounded line ranges
|
||||
-> for a directory: return direct children in directory-first alphabetical order
|
||||
-> page directory results with one-based offset and next cursor
|
||||
```
|
||||
|
||||
V2 `bash` uses the normal permission semantics: configured agent rules plus saved project approvals, with `ask` as the default when no rule matches. Bash is not sandboxed: the spawned shell runs with the host user's filesystem, process, and network authority. Structured external `workdir` resolution remains an enforced `external_directory` authority check. Best-effort scans of absolute command arguments produce advisory warnings only; they are not sandbox boundaries and do not request or enforce `external_directory` approval.
|
||||
|
||||
The first V2 `apply_patch` leaf supports add, update, and delete hunks. It parses every hunk, resolves every mutation target, approves external directories, approves one edit batch, and preflights approved update/delete targets before committing operations sequentially. A later commit-time failure leaves earlier operations applied and returns an explicit partial-application report. Moves and atomic rollback remain separate follow-ups rather than implied behavior.
|
||||
|
||||
### Current Runner Follow-Ups
|
||||
|
||||
- Keep eager structured local-tool settlement: durably record each complete call, start its child execution immediately, await all started settlements after step consumption, persist every result, and reload history once before continuation.
|
||||
- Buffer or coalesce streamed deltas before rewriting growing assistant projections.
|
||||
- Revisit additional covering indexes as larger-history query shapes become concrete.
|
||||
- Design any global multi-Session event stream separately; the finite history API deliberately reads one authorized Session aggregate and does not change global Event publication.
|
||||
- Decide whether UI-facing Session subscriptions should optionally interleave ephemeral deltas while connected without advancing the durable cursor.
|
||||
- Add provider-aware context control for provider-executed tool results. Generic text truncation cannot replace provider-native structured payloads that must round-trip exactly.
|
||||
|
||||
## Remove Dedicated `session.init` Route
|
||||
|
||||
The dedicated `POST /session/:sessionID/init` endpoint exists only as a compatibility wrapper around the normal `/init` command flow.
|
||||
|
||||
Current behavior:
|
||||
|
||||
- the route calls `SessionPrompt.command(...)`
|
||||
- it sends `Command.Default.INIT`
|
||||
- it does not provide distinct session-core behavior beyond running the existing init command in an existing session
|
||||
|
||||
V2 plan:
|
||||
|
||||
- remove the dedicated `session.init` endpoint
|
||||
- rely on the normal `/init` command flow instead
|
||||
- avoid reintroducing `Session.initialize`-style special cases in the session service layer
|
||||
Event replay ownership is separate from Session execution ownership. Local execution remains process-owned until clustering introduces an explicit placement and fencing protocol.
|
||||
|
|
|
|||
145
specs/v2/todo.md
145
specs/v2/todo.md
|
|
@ -1,145 +0,0 @@
|
|||
# TODO
|
||||
|
||||
ok we need to work towards a launch of v2 so we can get out of this rebuild phase
|
||||
|
||||
## Post-Hono cleanup - Kit
|
||||
|
||||
The opencode server has moved to the Effect HttpApi backend. Remaining work is
|
||||
mostly cleanup: delete compatibility shims, shrink Zod surfaces, and simplify
|
||||
test harnesses that used to compare Hono and HttpApi behavior.
|
||||
|
||||
## New Data Mode - Dax
|
||||
|
||||
This is mostly done. I'm working through modeling subagents, skill invocations
|
||||
and shell commands.
|
||||
|
||||
## Rework agent loop - Kit?
|
||||
|
||||
The first Effect-native local runner slice is implemented without bridging
|
||||
through legacy `SessionPrompt.loop(...)`:
|
||||
|
||||
- process-global `SessionExecution.resume(sessionID)` discovers Location from
|
||||
the Session read model
|
||||
- cached Location-scoped `SessionRunner` resolves one supported catalog model
|
||||
and issues one explicit `llm.stream(request)` step at a time
|
||||
- durable V2 projections record text, reasoning, provider failures, tool calls,
|
||||
tool results, and assistant output
|
||||
- owned local tool fibers and unresolved hosted calls settle before their step's
|
||||
single terminal event; cross-drain orphan recovery retains original assistant attribution
|
||||
- a scoped `ToolRegistry` advertises definitions and the first permission-checked
|
||||
`read` built-in
|
||||
- local continuation reloads projected history, and promoting new user input resets the selected agent's configured step allowance
|
||||
- concurrent resumes for one Session join one process-local run while different
|
||||
Sessions remain concurrent
|
||||
|
||||
Prompt admission now uses a durable `session_input` inbox rather than immediate
|
||||
transcript projection. `steer` inputs promote at the next safe step
|
||||
boundary while the current drain requires continuation. `queue` inputs remain in
|
||||
a FIFO until the Session would otherwise become idle and then promote one at a time.
|
||||
|
||||
Next reviewed slices:
|
||||
|
||||
- revisit per-step tool-call limits, output truncation, and operational
|
||||
backpressure before broadening exposure; eager local execution is deliberately
|
||||
unbounded in the current local slice while SQLite publication stays serialized
|
||||
- remove the public in-memory `@opencode-ai/llm` tool loop after replacing its
|
||||
remaining single-step native-adapter use with a narrow typed dispatcher
|
||||
- batch streamed deltas and add covering context indexes
|
||||
- expose replayable Session event cursors over HTTP and the generated SDK where remote consumers need them
|
||||
- integrate the new Job service with V2 tool execution: support background
|
||||
bash jobs and background agent dispatch with durable status observation,
|
||||
completion delivery, and explicit cancellation / continuation semantics
|
||||
- add durable/clustered interruption, retries, and stale-owner fencing only as
|
||||
their slices become concrete
|
||||
|
||||
### Deferred durable continuation recovery
|
||||
|
||||
Do not infer that ambiguous provider work is safe to retry from an advisory wake, an unmatched historical `session.execution.started` event, or a surviving `session.retry.scheduled` projection.
|
||||
The first inbox-driven runner intentionally omits outer physical-attempt markers
|
||||
until they have a concrete consumer and a complete recovery policy.
|
||||
|
||||
Design post-crash continuation recovery as one explicit slice. It should model:
|
||||
|
||||
- promoted input and projected-history state
|
||||
- queued-input promotion and steering assignment
|
||||
- physical-attempt preparation versus provider-dispatch ambiguity
|
||||
- required post-tool continuation across process loss
|
||||
- explicit `retry` and `abandon` decisions for unknown outcomes
|
||||
- bounded automatic retry only where provider and tool idempotency make it safe
|
||||
- retry budget, backoff, visible recovery status, startup discovery, and future
|
||||
clustered ownership fencing
|
||||
|
||||
Do not introduce an enclosing durable execution identity solely to group these
|
||||
facts; a process-local Session drain has no durable transcript boundary.
|
||||
|
||||
## Plugin API design - James?
|
||||
|
||||
We need to figure out how we want server plugins to work and what hooks are useful.
|
||||
|
||||
Some ideas:
|
||||
|
||||
- plugins get immer drafts so bad mutations can be thrown away
|
||||
- plugins get global "opencode" instance like in that post i showed
|
||||
- opencode instance has stuff like `opencode.session.prompt()` or
|
||||
`opencode.tool.register({...})`
|
||||
|
||||
## Rework Config - ???
|
||||
|
||||
We should do another pass on config to clean up any mistakes we made with it and
|
||||
simplify as much as possible. Old configs should get auto-converted to new
|
||||
|
||||
## Auth - ???
|
||||
|
||||
I have a basic auth system that can track any kind of auth, not just providers
|
||||
|
||||
## Model Database - ???
|
||||
|
||||
I have a basic model service that allows for models to be registered dynamically
|
||||
|
||||
## Provider - ???
|
||||
|
||||
Providers should register as plugins and autoload based on whatever logic they
|
||||
want / config. They should register models into model database
|
||||
|
||||
## Event - Kit
|
||||
|
||||
The self-contained durable `EventV2` core service is implemented. It owns
|
||||
sync-versioned persistence, transactional sequencing, pub/sub, replay, and
|
||||
replay-owner claims without relying on the old bus system.
|
||||
|
||||
Remaining slices:
|
||||
|
||||
- expose the embedded consumer-facing Session cursor API over HTTP and the
|
||||
generated SDK where remote consumers need it
|
||||
- keep replay-owner claims distinct from future clustered Session execution
|
||||
ownership and stale-runtime fencing
|
||||
|
||||
## Deferred hardening cleanup
|
||||
|
||||
Keep these visible, but do not block functionality slices on them unless a concrete
|
||||
failure appears during canary work:
|
||||
|
||||
- serialize database migration claiming across processes; current migration
|
||||
application is protected only by an in-process semaphore, so two processes
|
||||
starting against one SQLite database can still race
|
||||
- simplify process-local durable-tail wake lifecycle with Effect `RcMap` and one
|
||||
shared `PubSub.sliding<void>(1)` per active aggregate; keep SQLite cursor replay
|
||||
and subscribe-before-history semantics unchanged
|
||||
- page large durable aggregate replay reads instead of loading every row after a
|
||||
stale cursor into one array
|
||||
- decide whether connected tails need a periodic polling fallback for
|
||||
cross-process SQLite writers; current advisory wakes are intentionally
|
||||
process-local
|
||||
- stream-cap websearch body collection before parsing
|
||||
- add ripgrep execution timeout and bounded line framing
|
||||
- materialize or consistently reject unresolved URL and file attachment sources
|
||||
- decide stateless OpenAI Responses hosted-tool continuation behavior; reconstructed hosted output can replay as a stored `item_reference` when `store !== false`, while `store: false` intentionally omits the unavailable reference path
|
||||
- decide whether to preserve deprecated `@opencode-ai/llm` orchestration exports
|
||||
- preserve or alias renamed filesystem SDK generated type names if compatibility
|
||||
consumers require them
|
||||
- revisit syscall-level mutation confinement for hostile external processes
|
||||
(`openat`, `O_NOFOLLOW`, and descriptor-relative mutation where supported)
|
||||
|
||||
## Everything is hotreloadable - ???
|
||||
|
||||
Instead of needing to tear down things when something changes every service should emit granular events so services can react to them and reconfigure themselves. Allows frontend to receive these too, eg model.added. also prevents startup from blocking
|
||||
|
|
@ -1,38 +1,30 @@
|
|||
# V2 Tools
|
||||
|
||||
## Design
|
||||
Status: **Current semantic overview.** The Plugin package owns the public tool type; Core owns registration, settlement, and generic output bounding.
|
||||
|
||||
V2 has one opaque type for locally executable tools:
|
||||
## Tool Definitions Are Opaque
|
||||
|
||||
V2 has one opaque type for locally executable tools. Typed tools declare codecs, execution, and optional model-facing projection together:
|
||||
|
||||
```ts
|
||||
type Definition<Input, Output>
|
||||
type AnyTool = Definition<any, any>
|
||||
|
||||
const make: <
|
||||
Input extends Schema.Codec<any, any, never, never>,
|
||||
Output extends Schema.Codec<any, any, never, never>,
|
||||
>(config: {
|
||||
readonly description: string
|
||||
readonly input: Input
|
||||
readonly output: Output
|
||||
readonly execute: (
|
||||
input: Schema.Type<Input>,
|
||||
context: Tool.Context,
|
||||
) => Effect.Effect<Schema.Type<Output>, ToolFailure>
|
||||
readonly toModelOutput?: (input: {
|
||||
readonly input: Schema.Type<Input>
|
||||
readonly output: Output["Encoded"]
|
||||
}) => ReadonlyArray<Tool.Content>
|
||||
}) => Definition<Input, Output>
|
||||
const read = Tool.make({
|
||||
description: "Read a file",
|
||||
input: Schema.Struct({ path: Schema.String }),
|
||||
output: Schema.Struct({ content: Schema.String }),
|
||||
execute: ({ path }, context) => readFile(path, context),
|
||||
toModelOutput: ({ output }) => [{ type: "text", text: output.content }],
|
||||
})
|
||||
```
|
||||
|
||||
Application tools, built-ins, and statically authored plugin tools use this same constructor and execution contract.
|
||||
`structured` and `toStructuredOutput` may expose a smaller validated result than the complete execution output. Dynamic MCP and manifest tools use the same opaque representation with runtime JSON Schema.
|
||||
|
||||
Built-ins and statically authored plugin tools use this same constructor and execution contract.
|
||||
|
||||
`Tool.Definition` is opaque and has exactly one executor. Its schemas and executor are not public fields. The Tool module privately derives model definitions and interprets invocations for the registry; callers normally rely on `Tool.make` inference rather than naming the carrier type.
|
||||
|
||||
Input and output codecs are self-contained. Schema conversion cannot require services. Tool dependencies are acquired during construction and captured by `execute`.
|
||||
|
||||
## Invocation Context
|
||||
## Every Call Has Durable Identity
|
||||
|
||||
Every local tool receives the same concrete invocation context:
|
||||
|
||||
|
|
@ -40,18 +32,20 @@ Every local tool receives the same concrete invocation context:
|
|||
interface Tool.Context {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly assistantMessageID: Session.MessageID
|
||||
readonly toolCallID: ToolCall.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly toolCallID: string
|
||||
}
|
||||
```
|
||||
|
||||
`assistantMessageID` is the durable ID of the assistant message containing the call. The Session runner owns this association and supplies the complete context to the registry; the registry does not infer it.
|
||||
|
||||
Durable events call the invocation identifier `callID`; `Tool.Context.toolCallID` is the same value at the executor boundary.
|
||||
|
||||
Decoded tool input is passed separately to `execute`. Raw provider input and domain services do not belong in the invocation context.
|
||||
|
||||
Effect interruption is the cancellation mechanism. Tools may translate expected typed failures into `ToolFailure`, but must not translate interruption or defects into model-visible failures.
|
||||
|
||||
## Registration
|
||||
## Registrations Are Scoped
|
||||
|
||||
Tools are named when registered:
|
||||
|
||||
|
|
@ -64,7 +58,7 @@ yield *
|
|||
})
|
||||
```
|
||||
|
||||
The record key is the effective model-facing name. A reusable tool value has no intrinsic name.
|
||||
The record key is the authored name. Registration normalizes it before deriving the effective model-facing name. A reusable tool value has no intrinsic name.
|
||||
|
||||
```ts
|
||||
interface Tools {
|
||||
|
|
@ -74,11 +68,9 @@ interface Tools {
|
|||
}
|
||||
```
|
||||
|
||||
Tool names use a conservative provider-neutral grammar and are validated at registration. Provider-specific restrictions that cannot be validated generically fail during request preparation with an explicit model-compatibility error.
|
||||
Registration replaces unsupported name characters with `_` and reserves `execute` for Code Mode.
|
||||
|
||||
Process application tools and Location tools expose the same `register` operation but retain separate services and stores. Registration placement determines scope, precedence, and authority; it does not change the tool type.
|
||||
|
||||
A Location plugin receives only the narrow `Tools` registration capability, not the internal registry. Its installation effect runs once per applicable Location, acquires that Location's services, constructs its tools, and registers them in the plugin-owned Scope.
|
||||
A Location plugin receives only the narrow `Tools` registration capability, not the internal registry. Each activation acquires the Location's services, constructs its tools, and registers them in a fresh plugin-owned Scope.
|
||||
|
||||
Within one placement:
|
||||
|
||||
|
|
@ -87,9 +79,7 @@ Within one placement:
|
|||
- Closing the winner reveals the next-latest active registration.
|
||||
- Mutating the caller's registration record later does not change the captured registration.
|
||||
|
||||
Location registrations take precedence over process application registrations.
|
||||
|
||||
## Built-In Tools
|
||||
## Built-Ins Use The Same Contract
|
||||
|
||||
Built-ins use the same tool API while capturing trusted Location services:
|
||||
|
||||
|
|
@ -132,7 +122,7 @@ Trusted tools formulate and sequence permission requests. `PermissionV2` evaluat
|
|||
|
||||
Sharing a tool type does not imply equal authority. Built-ins and trusted Location plugins may capture services that are not available to application tools.
|
||||
|
||||
## Execution
|
||||
## Requests Capture Tool Values
|
||||
|
||||
The Location-scoped registry owns effective lookup and settlement. For each local call it:
|
||||
|
||||
|
|
@ -142,7 +132,8 @@ The Location-scoped registry owns effective lookup and settlement. For each loca
|
|||
4. Encodes the returned output with the output codec.
|
||||
5. Projects encoded output into model-facing content.
|
||||
6. Bounds the complete model-facing output.
|
||||
7. Returns the settlement and managed-output references to the runner, which persists them durably.
|
||||
7. Runs `execute.after` hooks with the bounded settlement.
|
||||
8. Returns the settlement to the runner for durable publication.
|
||||
|
||||
Invalid input never invokes the tool. Invalid output never produces a successful settlement.
|
||||
|
||||
|
|
@ -150,15 +141,15 @@ Invalid input never invokes the tool. Invalid output never produces a successful
|
|||
|
||||
Each model request captures the effective registered `Tool` value for every advertised name. Settlement executes those captured values; later registration changes affect later requests.
|
||||
|
||||
## Output Bounding
|
||||
## Producers And The Registry Own Different Limits
|
||||
|
||||
Tools return complete validated domain output. They do not truncate model-facing output or manage retention files.
|
||||
Producers may cap capture or spool data before a complete tool result exists. For example, a process tool may retain output it cannot keep in memory. Producer limits must report their own loss accurately; they are separate from registry bounding and cannot claim to reconstruct bytes already discarded.
|
||||
|
||||
After projection, one generic settlement boundary bounds the channel actually sent to the provider. When content exists, only its textual parts are measured; structured metadata is retained unchanged without being double-counted, and native media remains unchanged under producer-owned limits. When content is empty, the structured output is measured. Oversized provider-facing text or structured output is retained in managed storage and replaced with a bounded text preview while structured metadata and media are preserved; if complete retention fails, settlement fails operationally rather than publishing lossy success. Managed paths never appear in `Tool.make`, tool output schemas, or projection callbacks solely for retention bookkeeping.
|
||||
After projection, the registry bounds the channel sent to the provider. When content exists, only its textual parts are measured; structured metadata is retained unchanged without being double-counted, and native media remains unchanged under producer-owned limits. When content is empty, the structured output is measured. Oversized provider-facing text or structured output is retained in managed storage and replaced with a bounded text preview while structured metadata and media are preserved; if complete retention fails, settlement fails operationally rather than publishing lossy success. Managed paths never appear in `Tool.make`, tool output schemas, or projection callbacks solely for retention bookkeeping.
|
||||
|
||||
Model-output bounding is not producer memory management. Processes and streaming sources may need separate capture or spooling limits before a tool result exists. Those limits must be modeled at the producer boundary and must not masquerade as model-output truncation. A producer cannot claim a complete retained output after it has already discarded bytes.
|
||||
`execute.after` hooks receive the bounded settlement and its internal managed paths. Hooks may deliberately transform that settlement; the registry does not apply a second bounding pass afterward.
|
||||
|
||||
## Failure Semantics
|
||||
## Failures Preserve Interruptions
|
||||
|
||||
Outcomes remain distinct:
|
||||
|
||||
|
|
@ -177,9 +168,3 @@ Leaf tools translate only errors they deliberately classify as recoverable. Broa
|
|||
- **Scoped registration:** closing a Scope removes exactly its registration and reveals any prior active overlay.
|
||||
- **Captured execution:** a call executes the registered `Tool` value advertised in its model request.
|
||||
- **Storage encapsulation:** domain output does not change according to model-output bounding or retention policy.
|
||||
|
||||
## Follow-Up
|
||||
|
||||
Location plugin installation should receive the same narrow `Tools` capability. That requires a separate Location-layer ordering change so built-ins register before plugins without introducing a `PluginBoot -> Tools -> PluginBoot` dependency cycle. The carrier, registrar, and plugin-owned Scope semantics are already suitable; no tool-specific plugin hook is needed.
|
||||
|
||||
Session's current public result shape still exposes managed `outputPaths`. Extending storage encapsulation across the public Session API requires a separate opaque managed-output reference design; paths are not entirely internal today.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue