Compare commits

..

3 Commits

Author SHA1 Message Date
Aiden Cline b712f3bb27 fix(core): use long mcp tool timeout 2026-07-03 12:27:31 -05:00
Aiden Cline c0413cdefa fix(core): disable mcp tool call timeout 2026-07-03 12:25:09 -05:00
Aiden Cline 2c5cc78cd5 fix(core): keep mcp elicitations alive 2026-07-03 12:16:08 -05:00
378 changed files with 6434 additions and 12464 deletions
+2 -3
View File
@@ -155,10 +155,9 @@ const table = sqliteTable("session", {
- 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. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
- 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 `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 provider turn and reload projected history before durable continuation. 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.
- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn 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 provider-turn allowance; a batch of steers resets it once.
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
- Keep the System Context algebra and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Checkpoint persistence Session-owned. The runner composes all context producers explicitly in `loadSystemContext`; there is no context registry.
- The durable Applied record is what the model was last told, per source. Reconcile narrates drift as chronological System updates and never rewrites the baseline; only completed compaction rebaselines, and move or committed revert resets the checkpoint. Unavailable sources keep the model's prior belief, blocking only a session's first baseline.
+23 -38
View File
@@ -9,7 +9,7 @@ The structured collection of contextual facts presented to the model as initial
_Avoid_: System prompt
**Session History**:
The projected chronological conversation selected for a **Step** after applying the active compaction and **Context Epoch** cutoffs.
The projected chronological conversation selected for a provider turn after applying the active compaction and **Context Epoch** cutoffs.
_Avoid_: Session Context
**Context Source**:
@@ -31,13 +31,13 @@ The full **System Context** rendered at the start of a **Context Epoch**.
_Avoid_: Live system prompt
**Context Snapshot**:
The overwriteable model-hidden JSON state used to compare each **Context Source** with the value last admitted to a **Step**.
The overwriteable model-hidden JSON state used to compare each **Context Source** with the value last admitted to a provider turn.
**Unavailable Context**:
An expected temporary inability to observe a **Context Source** value; the runtime retains its prior effective state and emits no update, or omits it until first successfully loaded.
**Safe Step Boundary**:
The point immediately before a provider request, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically.
**Safe Provider-Turn Boundary**:
The point immediately before a provider call, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically.
**Admitted Prompt**:
A durable user input accepted into the Session inbox but not yet included in **Session History**.
@@ -45,24 +45,11 @@ A durable user input accepted into the Session inbox but not yet included in **S
**Prompt Promotion**:
The durable transition that removes an **Admitted Prompt** from pending input and appends its user message to **Session History**.
**Step**:
One logical LLM call spanning pre-flight context checkpoint preparation, input promotion, request build, and compaction check; the provider stream; and tool settlement.
_Avoid_: provider turn, turn (unqualified)
**Physical Attempt**:
One actual provider request on the wire in service of a **Step**; most Steps have one Physical Attempt, while overflow-triggered compaction recovery may give one Step two.
**Assistant Turn**:
A reserved name for the not-yet-modeled unit containing all **Steps** from prompt promotion until the assistant yields the floor; do not reify it until something durable needs it.
**Settlement**:
The terminal transition for a unit of work: Step and tool settlement are durable, while drain and execution settlement are coordinator-observed.
**Execution**:
One session-scoped coordinator busy period from first wake until idle. An Execution is process-local coordination rather than a durable domain entity.
**Provider Turn**:
One request to a model provider and the response projected from that request.
**Session Drain**:
One process-local execution span that promotes eligible input and runs required **Steps** until no immediate continuation remains. A Session Drain has no durable identity or transcript boundary.
One process-local execution span that promotes eligible input and runs required **Provider Turns** until no immediate continuation remains. A Session Drain has no durable identity or transcript boundary.
**Model Tool Output**:
The bounded projection of a Core-executed tool result persisted in Session history and replayed to the model. A tool may shape this projection semantically, but the Tool Registry enforces the final size limit.
@@ -102,25 +89,23 @@ _Avoid_: Response envelope
- A **System Context** is an opaque carrier composed from zero or more **Context Sources**.
- **Session History** contains projected conversational messages and admitted **Mid-Conversation System Messages**; the active **Baseline System Context** remains separate provider-request state.
- The **System Context Registry** uses stable-keyed scoped contributions to assemble the current **System Context**; contributor removal naturally removes its sources at the next **Safe Step Boundary**.
- The **System Context Registry** uses stable-keyed scoped contributions to assemble the current **System Context**; contributor removal naturally removes its sources at the next **Safe Provider-Turn Boundary**.
- A changed **Context Source** may produce one **Mid-Conversation System Message** containing its newly effective state.
- A **Mid-Conversation System Message** persists the exact combined rendered text sent to the model.
- The current **Context Snapshot** advances atomically with the corresponding durable **Mid-Conversation System Message**.
- A **Context Snapshot** stores one codec-encoded JSON value and, for removable dynamic sources, a pre-rendered removal message per stable **Context Source** key.
- Changes from multiple **Context Sources** admitted at one safe boundary combine into one **Mid-Conversation System Message**.
- Context changes are sampled and admitted lazily at a **Safe Step Boundary**, never pushed asynchronously when their source changes.
- At a **Safe Step Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**.
- Context changes are sampled and admitted lazily at a **Safe Provider-Turn Boundary**, never pushed asynchronously when their source changes.
- At a **Safe Provider-Turn Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**.
- An **Admitted Prompt** is replayable pending input, not yet model-visible **Session History**.
- **Prompt Promotion** atomically consumes the pending inbox entry and appends its model-visible user message.
- Steering prompts promote at the next **Safe Step Boundary** while the current **Session Drain** still requires continuation. Promoting any newly admitted user input resets the selected agent's step allowance; multiple prompts promoted at one boundary reset it once.
- Steering prompts promote at the next **Safe Provider-Turn Boundary** while the current **Session Drain** still requires continuation. Promoting any newly admitted user input resets the selected agent's provider-turn allowance; multiple prompts promoted at one boundary reset it once.
- A queued prompt does not promote while the current **Session Drain** requires continuation. The runner promotes one queued prompt when the Session would otherwise become idle, then reevaluates continuation before promoting another.
- A **Session Drain** is process-local coordination rather than a durable domain entity. Durable recovery must reason from prompts, projected history, physical attempts, and tool state rather than inventing an enclosing execution identity.
- An **Execution** contains one or more **Session Drains**; a **Session Drain** contains one reserved assistant-turn span at a time; that span contains **Steps**; and each **Step** contains one or more **Physical Attempts** plus any tool calls it requires.
- A **Step** record covers only the model-visible span from first assistant output through tool settlement; pre-flight leaves no record, and one Step settles at most one record.
- The first **Step** renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the Step instead of persisting an incomplete baseline.
- A **Session Drain** is process-local coordination rather than a durable domain entity. Durable recovery must reason from prompts, projected history, provider attempts, and tool state rather than inventing an enclosing execution identity.
- The first provider turn renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the turn instead of persisting an incomplete baseline.
- Initial **System Context** preparation precedes the first durable input promotion so an unavailable baseline leaves that input pending and retryable; ordinary reconciliation remains after promotion.
- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Snapshot**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history.
- A newly registered core or plugin-defined **Context Source** absent from the current snapshot emits its baseline rendering once at the next **Safe Step Boundary**.
- A newly registered core or plugin-defined **Context Source** absent from the current snapshot emits its baseline rendering once at the next **Safe Provider-Turn Boundary**.
- **Context Source** keys are stable and namespaced; duplicate keys fail composition. `SystemContext.combine(...)` preserves caller order; the **System Context Registry** evaluates producers concurrently and combines them in stable contribution-key order so rendered context remains deterministic.
- Each **Context Source** loader returns one coherent typed value. `SystemContext.make(...)` hides that value type so differently typed sources compose uniformly. Its codec compares and stores that value; its pure renderers produce model-visible baseline, update, and removal text only when needed.
- `SystemContext.initialize(...)` observes a composed **System Context** once and produces a fresh **Baseline System Context** with its **Context Snapshot**.
@@ -128,26 +113,26 @@ _Avoid_: Response envelope
- `SystemContext.replace(...)` renders a fresh generation after completed compaction or another baseline-replacing transition; it reports replacement blocked while previously admitted context is unavailable.
- **Unavailable Context** uses stale-while-revalidate semantics and is distinct from a successfully loaded absence, which may emit removal text.
- Ordinary **Context Source** loaders return values directly; loaders that intentionally use stale-while-revalidate may explicitly return **Unavailable Context**.
- Nested project instruction discovery after successful reads remains a follow-up; when implemented, discovered instructions must be admitted durably at the next **Safe Step Boundary**.
- Nested project instruction discovery after successful reads remains a follow-up; when implemented, discovered instructions must be admitted durably at the next **Safe Provider-Turn Boundary**.
- Location-scoped services naturally re-resolve effective context when a moved session next runs in its destination location.
- Moving a Session clears its active **Context Epoch**, so the destination must initialize a complete baseline before another prompt can promote.
- Instruction discovery, source identity, persistence, and file loading belong to the instruction service; the **System Context** abstraction only composes effectful producers and renders loaded values.
- The first instruction-service slice observes global and upward project `AGENTS.md` files as one ordered aggregate **Context Source** at each **Safe Step Boundary**.
- The first instruction-service slice observes global and upward project `AGENTS.md` files as one ordered aggregate **Context Source** at each **Safe Provider-Turn Boundary**.
- Built-in and instruction context producers register through the **System Context Registry** with stable contribution keys. Plugin-defined context registration and hot-reload lifecycle remain a follow-up built on the same scoped registry seam.
- Selected-agent available-skill guidance is a **Context Source** composed with Location-wide registry sources immediately before Context Epoch admission. It lists only names and descriptions permitted for that agent; skill bodies and locations are exposed only through the permission-checked `skill` tool.
- The selected agent and model are sampled when a **Step** starts. Changes admitted after that boundary apply to the next Step and do not restart the current Step.
- The selected agent and model are sampled when a provider turn starts. Changes admitted after that boundary apply to the next provider turn and do not restart the current turn.
- Selected-agent available-skill guidance remains a **Context Source**. An agent switch that changes that guidance produces a **Mid-Conversation System Message** while preserving the current baseline.
- Local tool authorization and pending permission requests retain the effective agent of the **Step** that issued the call; a later agent switch cannot change that call's policy.
- Context source changes never wake idle sessions; the next naturally scheduled **Safe Step Boundary** loads and compares current values lazily.
- Once admitted, a **Mid-Conversation System Message** remains durable even if the following **Physical Attempt** fails and is replayed unchanged on retry.
- Local tool authorization and pending permission requests retain the effective agent of the provider turn that issued the call; a later agent switch cannot change that call's policy.
- Context source changes never wake idle sessions; the next naturally scheduled **Safe Provider-Turn Boundary** loads and compares current values lazily.
- Once admitted, a **Mid-Conversation System Message** remains durable even if the following provider attempt fails and is replayed unchanged on retry.
- **Mid-Conversation System Messages** remain durable Session-message history; normal user-facing transcript surfaces may hide them.
- The date **Context Source** initially preserves host-local calendar-date behavior; a configured user timezone may replace that default later.
- A **Context Epoch** begins with one immutable **Baseline System Context**.
- A **Baseline System Context** is stored durably and reused verbatim across process restarts within its **Context Epoch**.
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
- Completed compaction starts a new **Context Epoch** on the next **Physical Attempt**, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next **Step**.
- **Native Continuation Metadata** remains in durable history. Step projection includes it only for a successful exact originating provider/model match; failed Steps and incompatible models omit opaque metadata, while non-empty visible reasoning lowers to ordinary assistant text after a model switch. This conservative relation may widen only when recorded provider tests establish compatibility.
- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn.
- **Native Continuation Metadata** remains in durable history. Provider-turn projection includes it only for a successful exact originating provider/model match; failed turns and incompatible models omit opaque metadata, while non-empty visible reasoning lowers to ordinary assistant text after a model switch. This conservative relation may widen only when recorded provider tests establish compatibility.
- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding.
- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing.
- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`.
+14 -42
View File
@@ -14,7 +14,6 @@
},
"devDependencies": {
"@actions/artifact": "5.0.1",
"@ast-grep/cli": "0.44.0",
"@tsconfig/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@typescript/native-preview": "catalog:",
@@ -122,14 +121,14 @@
},
"packages/client": {
"name": "@opencode-ai/client",
"version": "1.17.13",
"dependencies": {
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*",
},
"devDependencies": {
"@effect/platform-node": "catalog:",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/httpapi-codegen": "workspace:*",
"@opencode-ai/server": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
@@ -598,6 +597,7 @@
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -694,7 +694,6 @@
"version": "1.17.13",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*",
@@ -796,7 +795,6 @@
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/simulation": "workspace:*",
"drizzle-orm": "catalog:",
"effect": "catalog:",
},
@@ -850,21 +848,6 @@
"vite": "catalog:",
},
},
"packages/simulation": {
"name": "@opencode-ai/simulation",
"version": "1.17.13",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opentui/core": "catalog:",
"effect": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.17.13",
@@ -979,7 +962,6 @@
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/simulation": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
@@ -1254,22 +1236,6 @@
"@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
"@ast-grep/cli": ["@ast-grep/cli@0.44.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.44.0", "@ast-grep/cli-darwin-x64": "0.44.0", "@ast-grep/cli-linux-arm64-gnu": "0.44.0", "@ast-grep/cli-linux-x64-gnu": "0.44.0", "@ast-grep/cli-win32-arm64-msvc": "0.44.0", "@ast-grep/cli-win32-ia32-msvc": "0.44.0", "@ast-grep/cli-win32-x64-msvc": "0.44.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-Jf4PuP7XjzsMa3m9gYxmzV8KyWZc4w1ZzKe/t0+90wWxmSasQJe6AtMkJxHEi98MGgfAF1nWziqjDd0/6EsBjA=="],
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bF7euu/hF/cYg4510z8110vh60rrqfrBdsfRqVGd6xqNSPENu7CJnTVN/Z4Nk5U1NM8YKzUD+dYx1ySUJ0CUNQ=="],
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.44.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0fI9caQGp1dFcmBATNlVytIRdAeYb91v1D2xjMIi1bSX+l8Uj846JUiaimUGBuBZmyFq+BScoWM4RnprEmZMpQ=="],
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JB6EUnqEtGGtyg1GqNquld/++1CvaWD7r84IwwhddX1qx0NmDoHyn2mKd8vnQ24Z0RkV3g7y7foMLakELbGtDw=="],
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rNL0LsI682D9EMzfaGVEtZa1xaqTtGb2I+Zk4ZzidX6u+fF7f79wdqyKahKjXzoIrGkuhkoL3gcyLKAtQd9+qg=="],
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.44.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-lqD0MhGQAddh2YoV/brKQ6GVcFLmRiTBwIElutwedaUvRCdasTGukFPYuSWk/iI8Kv19xom6s7l+mGuZ7v+xwQ=="],
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.44.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-ZJrnS+2OkNfwyr6yrN69glP67uybBxDvl9mqZvh1J44vB3OFn9U9c+cVAoZIAo7JD5F4rZNxwyu3gcy4+xuwEA=="],
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OJEo7f95YYaSuS1byUB7ZctbzxoA7/wCoAol+pt6pvfdW/8Wq+L1qU28glwx7dQ0HgTsnPZbWpXQwmZpCBHhZg=="],
"@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="],
"@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
@@ -2036,8 +2002,6 @@
"@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"],
"@opencode-ai/simulation": ["@opencode-ai/simulation@workspace:packages/simulation"],
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
@@ -3526,7 +3490,7 @@
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
@@ -6084,8 +6048,6 @@
"@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
"@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
@@ -6152,6 +6114,8 @@
"@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@@ -6364,6 +6328,8 @@
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
@@ -6394,6 +6360,8 @@
"node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="],
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
@@ -6460,6 +6428,8 @@
"serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="],
"shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
@@ -7108,6 +7078,8 @@
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
-3
View File
@@ -14,8 +14,6 @@
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src",
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
"typecheck": "bun turbo typecheck",
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
"postinstall": "bun run --cwd packages/core fix-node-pty",
@@ -97,7 +95,6 @@
},
"devDependencies": {
"@actions/artifact": "5.0.1",
"@ast-grep/cli": "0.44.0",
"@tsconfig/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@typescript/native-preview": "catalog:",
@@ -177,7 +177,7 @@ export function DialogCustomProvider(props: Props) {
>
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
<div class="px-2.5 flex gap-4 items-center">
<ProviderIcon id="session.synthetic" class="size-5 shrink-0 icon-strong-base" />
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
<div class="text-16-medium text-text-strong">{language.t("provider.custom.title")}</div>
</div>
@@ -226,7 +226,7 @@ const SettingsProvidersContent: Component = () => {
>
<div class="flex flex-col min-w-0">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
<ProviderIcon id="session.synthetic" class="size-5 shrink-0 icon-strong-base" />
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{language.t("provider.custom.title")}</span>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</div>
@@ -223,7 +223,7 @@ export const SettingsProvidersV2: Component = () => {
<div class="settings-v2-provider-row" data-component="custom-provider-section">
<div class="settings-v2-provider-lead">
<ProviderIcon
id="session.synthetic"
id="synthetic"
width={PROVIDER_ICON_SIZE}
height={PROVIDER_ICON_SIZE}
class="settings-v2-provider-icon shrink-0"
@@ -7,7 +7,7 @@ describe("file watcher invalidation", () => {
const refresh: string[] = []
invalidateFromWatcher(
{
type: "filesystem.changed",
type: "file.watcher.updated",
properties: {
file: "src/new.ts",
event: "add",
@@ -32,7 +32,7 @@ describe("file watcher invalidation", () => {
invalidateFromWatcher(
{
type: "filesystem.changed",
type: "file.watcher.updated",
properties: {
file: "src/open.ts",
event: "change",
@@ -63,7 +63,7 @@ describe("file watcher invalidation", () => {
invalidateFromWatcher(
{
type: "filesystem.changed",
type: "file.watcher.updated",
properties: {
file: "src",
event: "change",
@@ -81,7 +81,7 @@ describe("file watcher invalidation", () => {
invalidateFromWatcher(
{
type: "filesystem.changed",
type: "file.watcher.updated",
properties: {
file: "src/file.ts",
event: "change",
@@ -111,7 +111,7 @@ describe("file watcher invalidation", () => {
invalidateFromWatcher(
{
type: "filesystem.changed",
type: "file.watcher.updated",
properties: {
file: ".git/index.lock",
event: "change",
+1 -1
View File
@@ -16,7 +16,7 @@ type WatcherOps = {
}
export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) {
if (event.type !== "filesystem.changed") return
if (event.type !== "file.watcher.updated") return
const props =
typeof event.properties === "object" && event.properties ? (event.properties as Record<string, unknown>) : undefined
const rawPath = typeof props?.file === "string" ? props.file : undefined
+1 -1
View File
@@ -820,7 +820,7 @@ export default function Page() {
)
const stopVcs = sdk().event.listen((evt) => {
if (evt.details.type !== "filesystem.changed") return
if (evt.details.type !== "file.watcher.updated") return
const props =
typeof evt.details.properties === "object" && evt.details.properties
? (evt.details.properties as Record<string, unknown>)
+2 -1
View File
@@ -4,6 +4,7 @@ import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Service } from "@opencode-ai/client/effect"
import { ServiceConfig } from "../../services/service-config"
import type { Transport } from "@opencode-ai/client/effect"
const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"])
@@ -60,7 +61,7 @@ export function rawRequest(input: readonly string[]) {
}
function resolveRequest(
transport: Service.Transport,
transport: Transport,
input: readonly string[],
params: Record<string, string>,
) {
@@ -1,5 +1,5 @@
import { EOL } from "os"
import { Effect } from "effect"
import * as Effect from "effect/Effect"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
+7 -43
View File
@@ -1,9 +1,9 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Effect, Option, Redacted } from "effect"
import { Effect, Option } from "effect"
import { Service } from "@opencode-ai/client/effect"
import { Env } from "../../env"
import type { Transport } from "@opencode-ai/client/effect"
import { ServiceConfig } from "../../services/service-config"
import { Standalone } from "../../services/standalone"
import { Updater } from "../../services/updater"
@@ -19,29 +19,11 @@ export default Runtime.handler(Commands, (input) =>
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
const transport = yield* Effect.gen(function* () {
if (server !== undefined) {
const password = yield* Env.password
const explicit = {
const password = process.env["OPENCODE_SERVER_PASSWORD"]
return {
url: server,
headers: password
? { authorization: "Basic " + btoa("opencode:" + Redacted.value(password)) }
: undefined,
} satisfies Service.Transport
// Fail loudly before entering the TUI: an explicit server that is
// unreachable or rejects auth should not present as reconnect churn.
const response = yield* Effect.tryPromise(() =>
fetch(new URL("/api/health", server), { headers: explicit.headers, signal: AbortSignal.timeout(5_000) }),
).pipe(Effect.mapError((cause) => new Error(`Could not reach server at ${server}`, { cause })))
if (response.status === 401)
return yield* Effect.fail(
new Error(
password
? `Server at ${server} rejected the password`
: `Server at ${server} requires a password; set OPENCODE_PASSWORD`,
),
)
if (!response.ok)
return yield* Effect.fail(new Error(`Server at ${server} responded with status ${response.status}`))
return explicit
headers: password ? { authorization: "Basic " + btoa("opencode:" + password) } : undefined,
} satisfies Transport
}
if (input.standalone) return yield* Standalone.transport()
const options = yield* ServiceConfig.options()
@@ -63,24 +45,6 @@ export default Runtime.handler(Commands, (input) =>
}).pipe(Effect.provide(NodeFileSystem.layer)),
)
: () => Promise.resolve(transport)
// Restart the managed service in place; start() resolves once the
// replacement is healthy and the reconnect loop reattaches on its own.
// Only meaningful in service mode: --server is not ours to restart and a
// standalone child cannot be respawned.
const reload = serviceOptions
? () =>
Effect.runPromise(
Effect.gen(function* () {
yield* Service.stop(serviceOptions)
yield* Service.start(serviceOptions)
}).pipe(Effect.provide(NodeFileSystem.layer)),
)
: undefined
yield* runTui(
transport,
{ continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
discover,
reload,
)
yield* runTui(transport, { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, discover)
}),
)
@@ -1,5 +1,5 @@
import { EOL } from "node:os"
import { Effect } from "effect"
import * as Effect from "effect/Effect"
import { createOpencodeClient, type McpServer } from "@opencode-ai/sdk/v2/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
@@ -1,4 +1,4 @@
import { Effect } from "effect"
import * as Effect from "effect/Effect"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
+22 -31
View File
@@ -3,9 +3,9 @@ import { Credential } from "@opencode-ai/core/credential"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { Project } from "@opencode-ai/core/project"
import { Global } from "@opencode-ai/core/global"
import { Context, Effect, FileSystem, Layer, Option, Redacted, Schedule, Schema } from "effect"
import { Context, FileSystem, Layer, Option, Schedule, Schema } from "effect"
import * as Effect from "effect/Effect"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { createServer } from "node:http"
import { createRoutes } from "@opencode-ai/server/routes"
@@ -14,8 +14,6 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Service } from "@opencode-ai/client/effect"
import { Env } from "../../env"
import { ServiceConfig } from "../../services/service-config"
import { Updater } from "../../services/updater"
import { randomBytes, randomUUID } from "crypto"
@@ -27,19 +25,12 @@ export default Runtime.handler(
if (input.service) yield* Effect.sync(() => process.chdir(Global.Path.home))
return yield* Effect.scoped(
Effect.gen(function* () {
const standalonePassword = yield* Env.password
// Keep the lease credential out of the environment inherited by any
// process this server spawns.
if (input.stdio) {
delete process.env.OPENCODE_PASSWORD
delete process.env.OPENCODE_SERVER_PASSWORD
}
const standalonePassword = process.env.OPENCODE_SERVER_PASSWORD
if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
const config = input.service ? yield* ServiceConfig.read() : {}
const password = input.service
? yield* ServiceConfig.password()
: standalonePassword
? Redacted.value(standalonePassword)
: randomBytes(32).toString("base64url")
: standalonePassword || randomBytes(32).toString("base64url")
if (!password) return yield* Effect.fail(new Error("Missing server password"))
const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
const port = Option.isSome(input.port)
@@ -60,7 +51,7 @@ export default Runtime.handler(
if (!input.service && !input.stdio && !standalonePassword) console.log(`server password ${password}`)
const updater = yield* Updater.Service
yield* updater.check().pipe(Effect.schedule(Schedule.spaced("10 minutes")), Effect.forkScoped)
return yield* input.stdio ? waitForStdinClose() : Effect.never
return yield* (input.stdio ? waitForStdinClose() : Effect.never)
}).pipe(Effect.annotateLogs({ role: "server" })),
)
}),
@@ -72,11 +63,8 @@ export default Runtime.handler(
// not a startup lock: the atomic rename elects the latest writer, the watcher
// self-evicts losers, and the finalizer id-guard keeps an exiting server from
// deleting its successor's registration.
// Written and read through Service.Info so the file the server registers is
// provably the contract clients discover with.
const infoJson = Schema.fromJsonString(Service.Info)
const encodeInfo = Schema.encodeEffect(infoJson)
const decodeInfo = Schema.decodeUnknownEffect(infoJson)
const RegistrationId = Schema.Struct({ id: Schema.optional(Schema.String) })
const decodeRegistrationId = Schema.decodeUnknownEffect(Schema.fromJsonString(RegistrationId))
const register = Effect.fnUntraced(function* (address: HttpServer.Address) {
const fs = yield* FileSystem.FileSystem
@@ -85,17 +73,20 @@ const register = Effect.fnUntraced(function* (address: HttpServer.Address) {
const secret = yield* ServiceConfig.password()
const temp = file + "." + id + ".tmp"
yield* fs.makeDirectory(path.dirname(file), { recursive: true })
const encoded = yield* encodeInfo({
id,
version: InstallationVersion,
url: HttpServer.formatAddress(address),
pid: process.pid,
password: secret,
})
yield* fs.writeFileString(temp, encoded, { mode: 0o600 })
yield* fs.writeFileString(
temp,
JSON.stringify({
id,
version: InstallationVersion,
url: HttpServer.formatAddress(address),
pid: process.pid,
password: secret,
}),
{ mode: 0o600 },
)
yield* fs.rename(temp, file)
const currentID = fs.readFileString(file).pipe(
Effect.flatMap(decodeInfo),
Effect.flatMap(decodeRegistrationId),
Effect.map((info) => info.id),
Effect.orElseSucceed(() => undefined),
)
@@ -143,9 +134,9 @@ function listen(hostname: string, port: Option.Option<number>, password: string)
function bind(hostname: string, port: number, password: string) {
const server = createServer()
return Layer.build(
HttpRouter.serve(createRoutes(password), { disableListenLog: true }).pipe(
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node, Project.node]))),
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node]))),
),
).pipe(
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
@@ -1,5 +1,6 @@
import { EOL } from "os"
import { Effect, Option } from "effect"
import { Option } from "effect"
import * as Effect from "effect/Effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
@@ -1,5 +1,5 @@
import { EOL } from "os"
import { Effect } from "effect"
import * as Effect from "effect/Effect"
import { Service } from "@opencode-ai/client/effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
@@ -1,4 +1,4 @@
import { Effect } from "effect"
import * as Effect from "effect/Effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
@@ -1,5 +1,5 @@
import { EOL } from "os"
import { Effect } from "effect"
import * as Effect from "effect/Effect"
import { Service } from "@opencode-ai/client/effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
@@ -1,5 +1,5 @@
import { EOL } from "os"
import { Effect } from "effect"
import * as Effect from "effect/Effect"
import { Service } from "@opencode-ai/client/effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
@@ -1,4 +1,4 @@
import { Effect } from "effect"
import * as Effect from "effect/Effect"
import { Service } from "@opencode-ai/client/effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
@@ -1,4 +1,4 @@
import { Effect } from "effect"
import * as Effect from "effect/Effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
-15
View File
@@ -1,15 +0,0 @@
import { Config } from "effect"
// Every environment variable the CLI reads, in one place. Consumers yield
// these instead of touching process.env so the full surface stays visible,
// typed, and redacted where secret.
// The opencode server password: sent by clients connecting to an explicit
// --server, and adopted by a manually run or standalone server. The legacy
// name is still honored.
export const password = Config.redacted("OPENCODE_PASSWORD").pipe(
Config.orElse(() => Config.redacted("OPENCODE_SERVER_PASSWORD")),
Config.withDefault(undefined),
)
export * as Env from "./env"
+6 -15
View File
@@ -1,8 +1,9 @@
import { Effect, FileSystem, Scope } from "effect"
import { Command } from "effect/unstable/cli"
import * as Effect from "effect/Effect"
import * as Command from "effect/unstable/cli/Command"
import { Spec } from "./spec"
import { Global } from "@opencode-ai/core/global"
import { Updater } from "../services/updater"
import { FileSystem, Scope } from "effect"
export type Input<Value> =
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
@@ -11,21 +12,11 @@ export type Input<Value> =
? Input
: never
type RuntimeHandler = (
input: unknown,
) => Effect.Effect<void, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
type Loader<Node extends Spec.Any> = () => Promise<{
default: (
input: Input<Node>,
) => Effect.Effect<void, any, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
default: (input: Input<Node>) => Effect.Effect<void, any, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
}>
type ProvidedCommand = Command.Command<
string,
unknown,
unknown,
unknown,
FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope
>
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
? Loader<Node>
+1 -1
View File
@@ -1,4 +1,4 @@
import { Command } from "effect/unstable/cli"
import * as Command from "effect/unstable/cli/Command"
type Options<Config extends Command.Command.Config, Commands extends ReadonlyArray<Any>> = {
readonly description?: string
+6 -8
View File
@@ -1,7 +1,10 @@
#!/usr/bin/env bun
import { NodeFileSystem, NodeRuntime, NodeServices } from "@effect/platform-node"
import { Effect, Layer, Logger, References } from "effect"
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeServices from "@effect/platform-node/NodeServices"
import { NodeFileSystem } from "@effect/platform-node"
import * as Effect from "effect/Effect"
import { Layer, Logger, References } from "effect"
import { Commands } from "./commands/commands"
import { Runtime } from "./framework/runtime"
import { Logging } from "@opencode-ai/core/observability/logging"
@@ -43,12 +46,7 @@ const Handlers = Runtime.handlers(Commands, {
serve: () => import("./commands/handlers/serve"),
})
Effect.logInfo("cli starting", {
version: InstallationVersion,
channel: InstallationChannel,
local: InstallationLocal,
args: process.argv.slice(2),
}).pipe(
Effect.logInfo("cli starting", { version: InstallationVersion, channel: InstallationChannel, local: InstallationLocal }).pipe(
Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })),
Effect.annotateLogs({ role: "cli" }),
Effect.provide(Updater.layer),
+1 -1
View File
@@ -5,7 +5,7 @@ import { Effect, FileSystem, Schema } from "effect"
import { randomBytes } from "crypto"
import path from "path"
// The CLI's service configuration file, plus the Service.Options binding that
// The CLI's service configuration file, plus the ServiceOptions binding that
// points the client package's service operations at this CLI: which
// registration file (by channel), which version, and how to spawn opencode.
+1 -3
View File
@@ -15,9 +15,7 @@ function command(password: string) {
if (!compiled && entrypoint.length === 0) throw new Error("Failed to resolve CLI entrypoint")
return ChildProcess.make(process.execPath, [...entrypoint, "serve", "--stdio", "--port", "0"], {
cwd: process.cwd(),
// Explicit entry wins over anything inherited, so a user-exported
// OPENCODE_PASSWORD cannot shadow the child's lease credential.
env: { OPENCODE_PASSWORD: password },
env: { OPENCODE_SERVER_PASSWORD: password },
extendEnv: true,
// The server treats EOF on this pipe as the end of its ownership lease.
// The OS closes it even when the TUI is killed before Effect finalizers run.
+2 -8
View File
@@ -5,16 +5,11 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Global } from "@opencode-ai/core/global"
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
import { OpenCode } from "@opencode-ai/client/promise"
import type { Service } from "@opencode-ai/client/effect"
import type { Transport } from "@opencode-ai/client/effect"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import type { Args } from "@opencode-ai/tui/context/args"
export function runTui(
transport: Service.Transport,
args: Args,
discover?: () => Promise<Service.Transport>,
reload?: () => Promise<void>,
) {
export function runTui(transport: Transport, args: Args, discover?: () => Promise<Transport>) {
const config = TuiConfig.resolve({}, { terminalSuspend: false })
let disposeSlots: (() => void) | undefined
return Effect.gen(function* () {
@@ -38,7 +33,6 @@ export function runTui(
}
}
: undefined,
reload,
args,
config,
pluginHost: {
+5 -17
View File
@@ -1,30 +1,16 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/client",
"version": "1.17.13",
"private": true,
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/anomalyco/opencode.git",
"directory": "packages/client"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"exports": {
"./promise": "./src/promise/index.ts",
"./promise/api": "./src/promise/api.ts",
"./effect": "./src/effect/index.ts",
"./effect/api": "./src/effect/api.ts"
"./effect": "./src/effect/index.ts"
},
"scripts": {
"build": "bun run script/build-package.ts",
"generate": "bun run script/build.ts",
"check:generated": "bun run generate && git diff --exit-code -- src/promise/generated src/effect/generated src/effect/api",
"check:generated": "bun run generate && git diff --exit-code -- src/promise/generated src/effect/generated",
"test": "bun test --timeout 5000",
"typecheck": "tsgo --noEmit"
},
@@ -42,7 +28,9 @@
},
"devDependencies": {
"@effect/platform-node": "catalog:",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/httpapi-codegen": "workspace:*",
"@opencode-ai/server": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await $`rm -rf dist`
await $`bun tsc -p tsconfig.build.json`
+2 -2
View File
@@ -32,8 +32,8 @@ await Effect.runPromise(
fileURLToPath(new URL("../src/effect/generated", import.meta.url)),
),
write(
emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }),
fileURLToPath(new URL("../src/effect/api", import.meta.url)),
emitEffectShape(effectContract, { module: "@opencode-ai/protocol/client", api: "ClientApi" }),
fileURLToPath(new URL("../../plugin/src/v2/effect/generated", import.meta.url)),
),
],
{ concurrency: 3, discard: true },
-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
const originalText = await Bun.file("package.json").text()
const pkg = JSON.parse(originalText) as {
name: string
version: string
exports: Record<string, string | { import: string; types: string }>
}
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
console.log(`already published ${pkg.name}@${pkg.version}`)
process.exit(0)
}
try {
await $`bun run typecheck`
await $`bun run build`
pkg.exports = Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => {
if (typeof value !== "string") return [key, value]
return [
key,
{
import: value.replace("./src/", "./dist/").replace(/\.ts$/, ".js"),
types: value.replace("./src/", "./dist/").replace(/\.ts$/, ".d.ts"),
},
]
}),
)
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
await rm(tarball, { force: true })
await $`bun pm pack`
await $`npm publish ${tarball} --tag ${Script.channel} --access public`
} finally {
await Bun.write("package.json", originalText)
await rm(tarball, { force: true })
}
-8
View File
@@ -1,8 +0,0 @@
import type { ModelApi, ProviderApi } from "./api/api.js"
export type * from "./api/api.js"
export interface CatalogApi<E = never> {
readonly provider: ProviderApi<E>
readonly model: ModelApi<E>
}
+64 -77
View File
@@ -208,35 +208,23 @@ const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11I
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint4_12Input = {
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly id?: Endpoint4_12Request["payload"]["id"]
readonly command: Endpoint4_12Request["payload"]["command"]
}
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint4_12Input = { readonly sessionID: Endpoint4_12Request["params"]["sessionID"] }
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
raw["session.shell"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], command: input["command"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint4_14Input = { readonly sessionID: Endpoint4_14Request["params"]["sessionID"] }
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint4_15Input = {
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
readonly messageID: Endpoint4_15Request["payload"]["messageID"]
readonly files?: Endpoint4_15Request["payload"]["files"]
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly messageID: Endpoint4_14Request["payload"]["messageID"]
readonly files?: Endpoint4_14Request["payload"]["files"]
}
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
raw["session.revert.stage"]({
params: { sessionID: input["sessionID"] },
payload: { messageID: input["messageID"], files: input["files"] },
@@ -245,61 +233,61 @@ const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15I
Effect.map((value) => value.data),
)
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
raw["session.context.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
type Endpoint4_20Input = {
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
readonly key: Endpoint4_20Request["params"]["key"]
readonly value: Endpoint4_20Request["payload"]["value"]
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
type Endpoint4_19Input = {
readonly sessionID: Endpoint4_19Request["params"]["sessionID"]
readonly key: Endpoint4_19Request["params"]["key"]
readonly value: Endpoint4_19Request["payload"]["value"]
}
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
raw["session.context.entry.put"]({
params: { sessionID: input["sessionID"], key: input["key"] },
payload: { value: input["value"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly key: Endpoint4_21Request["params"]["key"]
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
type Endpoint4_20Input = {
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
readonly key: Endpoint4_20Request["params"]["key"]
}
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
raw["session.context.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint4_22Input = {
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly after?: Endpoint4_22Request["query"]["after"]
readonly follow?: Endpoint4_22Request["query"]["follow"]
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly after?: Endpoint4_21Request["query"]["after"]
readonly follow?: Endpoint4_21Request["query"]["follow"]
}
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
Stream.unwrap(
raw["session.log"]({
params: { sessionID: input["sessionID"] },
@@ -310,22 +298,22 @@ const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22I
),
)
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] }
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_25Input = {
readonly sessionID: Endpoint4_25Request["params"]["sessionID"]
readonly messageID: Endpoint4_25Request["params"]["messageID"]
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_24Input = {
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
readonly messageID: Endpoint4_24Request["params"]["messageID"]
}
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
@@ -344,20 +332,19 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
command: Endpoint4_9(raw),
skill: Endpoint4_10(raw),
synthetic: Endpoint4_11(raw),
shell: Endpoint4_12(raw),
compact: Endpoint4_13(raw),
wait: Endpoint4_14(raw),
revertStage: Endpoint4_15(raw),
revertClear: Endpoint4_16(raw),
revertCommit: Endpoint4_17(raw),
context: Endpoint4_18(raw),
listContextEntries: Endpoint4_19(raw),
putContextEntry: Endpoint4_20(raw),
removeContextEntry: Endpoint4_21(raw),
log: Endpoint4_22(raw),
interrupt: Endpoint4_23(raw),
background: Endpoint4_24(raw),
message: Endpoint4_25(raw),
compact: Endpoint4_12(raw),
wait: Endpoint4_13(raw),
revertStage: Endpoint4_14(raw),
revertClear: Endpoint4_15(raw),
revertCommit: Endpoint4_16(raw),
context: Endpoint4_17(raw),
listContextEntries: Endpoint4_18(raw),
putContextEntry: Endpoint4_19(raw),
removeContextEntry: Endpoint4_20(raw),
log: Endpoint4_21(raw),
interrupt: Endpoint4_22(raw),
background: Endpoint4_23(raw),
message: Endpoint4_24(raw),
})
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
+1 -17
View File
@@ -1,23 +1,8 @@
// TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
import type { Effect } from "effect"
export * from "./generated/index"
export type {
AgentApi,
AppApi,
CatalogApi,
CommandApi,
EventApi,
IntegrationApi,
ModelApi,
PluginApi,
ProviderApi,
ReferenceApi,
SessionApi,
SkillApi,
} from "./api.js"
export { Service } from "./service.js"
export type { Transport, ServiceOptions } from "./service.js"
export { Agent } from "@opencode-ai/schema/agent"
export { Command } from "@opencode-ai/schema/command"
export { Credential } from "@opencode-ai/schema/credential"
@@ -43,4 +28,3 @@ export { SessionMessage } from "@opencode-ai/schema/session-message"
export { Skill } from "@opencode-ai/schema/skill"
export { Prompt } from "@opencode-ai/schema/prompt"
export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
export type OpenCodeClient = Effect.Success<ReturnType<typeof import("./generated/client").make>>
+27 -27
View File
@@ -16,7 +16,7 @@ export type Transport = {
readonly headers?: RequestInit["headers"]
}
export type Options = {
export type ServiceOptions = {
// Absolute path to the service registration file. Defaults to
// opencode/service.json in the XDG state directory.
readonly file?: string
@@ -30,21 +30,21 @@ export type Options = {
// Read-only lookup: registration file plus health check and version gate.
// Never spawns; escalation to start() is the caller's policy.
export const discover = Effect.fn("service.discover")(function* (options: Options = {}) {
const info = yield* read(options.file)
if (info === undefined) return undefined
if (options.version !== undefined && info.version !== options.version) return undefined
const found = yield* probe(info)
export const discover = Effect.fn("service.discover")(function* (options: ServiceOptions = {}) {
const registration = yield* read(options.file)
if (registration === undefined) return undefined
if (options.version !== undefined && registration.version !== options.version) return undefined
const found = yield* probe(registration)
return found?.transport
})
// Idempotent ensure-running: reuses a healthy compatible server, replaces a
// version-mismatched one, and otherwise spawns the service command detached.
export const start = Effect.fn("service.start")(function* (options: Options = {}) {
export const start = Effect.fn("service.start")(function* (options: ServiceOptions = {}) {
const compatible = yield* discover(options)
if (compatible !== undefined) return compatible
const mismatched = yield* find(options)
if (mismatched !== undefined) yield* kill(mismatched.info, options).pipe(Effect.ignore)
if (mismatched !== undefined) yield* kill(mismatched.registration, options).pipe(Effect.ignore)
const [command, ...args] = options.command ?? ["opencode", "serve", "--service"]
if (command === undefined) return yield* Effect.fail(new Error("Missing service command"))
@@ -64,10 +64,10 @@ export const start = Effect.fn("service.start")(function* (options: Options = {}
)
})
export const stop = Effect.fn("service.stop")(function* (options: Options = {}) {
export const stop = Effect.fn("service.stop")(function* (options: ServiceOptions = {}) {
const fs = yield* FileSystem.FileSystem
const existing = yield* find(options)
if (existing !== undefined) yield* kill(existing.info, options)
if (existing !== undefined) yield* kill(existing.registration, options)
yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore)
})
@@ -80,18 +80,18 @@ function auth(password: string): RequestInit["headers"] {
return { authorization: "Basic " + btoa("opencode:" + password) }
}
export const Info = Schema.Struct({
const Registration = Schema.Struct({
id: Schema.optional(Schema.String),
version: Schema.optional(Schema.String),
url: Schema.String,
pid: Schema.Int.check(Schema.isGreaterThan(0)),
password: Schema.optional(Schema.String),
})
export type Info = typeof Info.Type
type Registration = typeof Registration.Type
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
// A missing or corrupt file means no valid info; callers treat both
// A missing or corrupt file means no valid registration; callers treat both
// the same (the registering server self-evicts, clients rediscover).
const read = Effect.fnUntraced(function* (file?: string) {
const fs = yield* FileSystem.FileSystem
@@ -101,14 +101,14 @@ const read = Effect.fnUntraced(function* (file?: string) {
})
type LocalService = {
readonly info: Info
readonly registration: Registration
readonly transport: Transport
}
const probe = Effect.fnUntraced(function* (info: Info) {
const headers = info.password === undefined ? undefined : auth(info.password)
const probe = Effect.fnUntraced(function* (registration: Registration) {
const headers = registration.password === undefined ? undefined : auth(registration.password)
const healthy = yield* Effect.tryPromise(() =>
fetch(new URL("/api/health", info.url), {
fetch(new URL("/api/health", registration.url), {
headers,
signal: AbortSignal.timeout(2_000),
}),
@@ -117,15 +117,15 @@ const probe = Effect.fnUntraced(function* (info: Info) {
Effect.orElseSucceed(() => false),
)
if (!healthy) return undefined
return { info, transport: { url: info.url, headers } } satisfies LocalService
return { registration, transport: { url: registration.url, headers } } satisfies LocalService
})
// Health-checked lookup without the version gate: lifecycle operations must be
// able to see (and replace or stop) a server from a different version.
const find = Effect.fnUntraced(function* (options: Options) {
const info = yield* read(options.file)
if (info === undefined) return undefined
return yield* probe(info)
const find = Effect.fnUntraced(function* (options: ServiceOptions) {
const registration = yield* read(options.file)
if (registration === undefined) return undefined
return yield* probe(registration)
})
// 50ms cadence bounded at ~5s, shared by stop escalation and start readiness.
@@ -142,22 +142,22 @@ const stopped = Effect.fnUntraced(function* (pid: number) {
return yield* Effect.fail(new Error(`Server process ${pid} is still running`))
})
function same(left: Info, right: Info) {
function same(left: Registration, right: Registration) {
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
}
const kill = Effect.fnUntraced(function* (info: Info, options: Options) {
const kill = Effect.fnUntraced(function* (info: Registration, options: ServiceOptions) {
// A stale registration may point at a PID that has since been reused by
// another process. Only signal the PID after authenticating the server.
const current = yield* find(options)
if (current === undefined || !same(current.info, info)) return
if (current === undefined || !same(current.registration, info)) return
yield* signal(info.pid, "SIGTERM")
const done = yield* stopped(info.pid).pipe(Effect.retry(poll), Effect.option)
if (Option.isSome(done)) return
const latest = yield* find(options)
if (latest === undefined || !same(latest.info, info)) return
if (latest === undefined || !same(latest.registration, info)) return
yield* signal(info.pid, "SIGKILL")
yield* stopped(info.pid).pipe(Effect.retry(poll))
})
-41
View File
@@ -1,41 +0,0 @@
import type {
AgentApi as EffectAgentApi,
CommandApi as EffectCommandApi,
EventApi as EffectEventApi,
IntegrationApi as EffectIntegrationApi,
ModelApi as EffectModelApi,
PluginApi as EffectPluginApi,
ProviderApi as EffectProviderApi,
ReferenceApi as EffectReferenceApi,
SessionApi as EffectSessionApi,
SkillApi as EffectSkillApi,
} from "../effect/api/api.js"
import type { Effect, Stream } from "effect"
type PromisifyOperation<Operation> = Operation extends (
...args: infer Args
) => Effect.Effect<infer Success, unknown, unknown>
? (...args: Args) => Promise<Success>
: Operation extends (...args: infer Args) => Stream.Stream<infer Success, unknown, unknown>
? (...args: Args) => AsyncIterable<Success>
: Operation
type PromisifyApi<Api> = {
readonly [Name in keyof Api]: PromisifyOperation<Api[Name]>
}
export type AgentApi = PromisifyApi<EffectAgentApi<unknown>>
export type CommandApi = PromisifyApi<EffectCommandApi<unknown>>
export type EventApi = PromisifyApi<EffectEventApi<unknown>>
export type IntegrationApi = PromisifyApi<EffectIntegrationApi<unknown>>
export type ModelApi = PromisifyApi<EffectModelApi<unknown>>
export type PluginApi = PromisifyApi<EffectPluginApi<unknown>>
export type ProviderApi = PromisifyApi<EffectProviderApi<unknown>>
export type ReferenceApi = PromisifyApi<EffectReferenceApi<unknown>>
export type SessionApi = PromisifyApi<EffectSessionApi<unknown>>
export type SkillApi = PromisifyApi<EffectSkillApi<unknown>>
export interface CatalogApi {
readonly provider: ProviderApi
readonly model: ModelApi
}
@@ -29,8 +29,6 @@ import type {
SessionSkillOutput,
SessionSyntheticInput,
SessionSyntheticOutput,
SessionShellInput,
SessionShellOutput,
SessionCompactInput,
SessionCompactOutput,
SessionWaitInput,
@@ -527,18 +525,6 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
shell: (input: SessionShellInput, requestOptions?: RequestOptions) =>
request<SessionShellOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/shell`,
body: { id: input["id"], command: input["command"] },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
},
requestOptions,
),
compact: (input: SessionCompactInput, requestOptions?: RequestOptions) =>
request<SessionCompactOutput>(
{
+579 -1040
View File
@@ -865,14 +865,6 @@ export type SessionSyntheticInput = {
export type SessionSyntheticOutput = void
export type SessionShellInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly id?: { readonly id?: string | undefined; readonly command: string }["id"]
readonly command: { readonly id?: string | undefined; readonly command: string }["command"]
}
export type SessionShellOutput = void
export type SessionCompactInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type SessionCompactOutput = void
@@ -976,27 +968,9 @@ export type SessionContextOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell"
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly metadata: { readonly [x: string]: JsonValue }
readonly time: {
readonly started: number | "Infinity" | "-Infinity" | "NaN"
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
}
}
readonly output?: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
readonly callID: string
readonly command: string
readonly output: string
}
| {
readonly id: string
@@ -1006,20 +980,23 @@ export type SessionContextOutput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string }
| { readonly type: "text"; readonly id: string; readonly text: string }
| {
readonly type: "reasoning"
readonly id: string
readonly text: string
readonly state?: { readonly [x: string]: JsonValue }
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
readonly type: "tool"
readonly id: string
readonly name: string
readonly executed?: boolean
readonly providerState?: { readonly [x: string]: JsonValue }
readonly providerResultState?: { readonly [x: string]: JsonValue }
readonly provider?: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
}
readonly state:
| { readonly status: "pending"; readonly input: string }
| {
@@ -1057,37 +1034,7 @@ export type SessionContextOutput = {
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly structured: { readonly [x: string]: JsonValue }
readonly error:
| {
readonly type: "provider.rate-limit"
readonly message: string
readonly retryAfterMs?: number
}
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: JsonValue
}
readonly time: {
@@ -1099,7 +1046,7 @@ export type SessionContextOutput = {
}
>
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish?: string
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -1107,56 +1054,7 @@ export type SessionContextOutput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly error?:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly retry?: {
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
}
readonly error?: { readonly type: "unknown"; readonly message: string }
}
| {
readonly type: "compaction"
@@ -1201,75 +1099,74 @@ export type SessionLogOutput =
| (
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.agent.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.agent.switched"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly agent: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly agent: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.model.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.model.switched"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.moved"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.moved"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly location: { readonly directory: string; readonly workspaceID?: string }
readonly subpath?: string
readonly subdirectory?: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.renamed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.renamed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly title: string }
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly title: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.forked"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.prompt.promoted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.prompt.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.forked"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly inputID: string
readonly parentID: string
readonly messageID?: string
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.prompted"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly prompt: {
readonly text: string
readonly files?: ReadonlyArray<{
@@ -1289,87 +1186,54 @@ export type SessionLogOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.succeeded"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.prompt.admitted"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly messageID: string
readonly prompt: {
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
readonly mime: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
}
readonly delivery: "steer" | "queue"
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.interrupted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "user" | "shutdown" | "superseded" }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.context.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.synthetic"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.context.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly text: string
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.synthetic"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly text: string
readonly description?: string
readonly metadata?: { readonly [x: string]: unknown }
@@ -1377,73 +1241,53 @@ export type SessionLogOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.skill.activated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly name: string; readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.shell.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.skill.activated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number
readonly metadata: { readonly [x: string]: unknown }
readonly time: { readonly started: number; readonly completed?: number }
}
readonly messageID: string
readonly name: string
readonly text: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.shell.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.shell.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number
readonly metadata: { readonly [x: string]: unknown }
readonly time: { readonly started: number; readonly completed?: number }
}
readonly output: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
readonly messageID: string
readonly callID: string
readonly command: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.shell.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly callID: string
readonly output: string
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.step.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly agent: string
@@ -1453,15 +1297,15 @@ export type SessionLogOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.step.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish: string
readonly cost: number
readonly tokens: {
readonly input: number
@@ -1475,96 +1319,52 @@ export type SessionLogOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.step.failed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.text.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly state?: { readonly [x: string]: unknown }
readonly textID: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.text.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly textID: string
readonly text: string
readonly state?: { readonly [x: string]: unknown }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.input.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.input.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
@@ -1573,12 +1373,12 @@ export type SessionLogOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.input.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.input.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
@@ -1587,28 +1387,31 @@ export type SessionLogOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.called"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.called"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
readonly tool: string
readonly input: { readonly [x: string]: unknown }
readonly executed: boolean
readonly state?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.progress"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.progress"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
@@ -1621,12 +1424,12 @@ export type SessionLogOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.success"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.success"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
@@ -1637,112 +1440,103 @@ export type SessionLogOutput =
>
readonly outputPaths?: ReadonlyArray<string>
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.failed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.retry.scheduled"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.reasoning.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly reasoningID: string
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.reasoning.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly reasoningID: string
readonly text: string
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.retried"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly attempt: number
readonly error: {
readonly message: string
readonly statusCode?: number
readonly isRetryable: boolean
readonly responseHeaders?: { readonly [x: string]: string }
readonly responseBody?: string
readonly metadata?: { readonly [x: string]: string }
}
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.compaction.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly reason: "auto" | "manual"
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.compaction.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly reason: "auto" | "manual"
readonly text: string
readonly recent: string
@@ -1750,12 +1544,12 @@ export type SessionLogOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.staged"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.revert.staged"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly revert: {
readonly messageID: string
@@ -1774,21 +1568,19 @@ export type SessionLogOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.cleared"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.revert.cleared"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
readonly data: { readonly timestamp: number; readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.committed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.revert.committed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly to: string }
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string }
}
)
| { readonly type: "log.synced"; readonly aggregateID: string; readonly seq?: number }
@@ -1869,27 +1661,9 @@ export type SessionMessageOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell"
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly metadata: { readonly [x: string]: JsonValue }
readonly time: {
readonly started: number | "Infinity" | "-Infinity" | "NaN"
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
}
}
readonly output?: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
readonly callID: string
readonly command: string
readonly output: string
}
| {
readonly id: string
@@ -1899,20 +1673,23 @@ export type SessionMessageOutput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string }
| { readonly type: "text"; readonly id: string; readonly text: string }
| {
readonly type: "reasoning"
readonly id: string
readonly text: string
readonly state?: { readonly [x: string]: JsonValue }
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
readonly type: "tool"
readonly id: string
readonly name: string
readonly executed?: boolean
readonly providerState?: { readonly [x: string]: JsonValue }
readonly providerResultState?: { readonly [x: string]: JsonValue }
readonly provider?: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
}
readonly state:
| { readonly status: "pending"; readonly input: string }
| {
@@ -1950,37 +1727,7 @@ export type SessionMessageOutput = {
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly structured: { readonly [x: string]: JsonValue }
readonly error:
| {
readonly type: "provider.rate-limit"
readonly message: string
readonly retryAfterMs?: number
}
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: JsonValue
}
readonly time: {
@@ -1992,7 +1739,7 @@ export type SessionMessageOutput = {
}
>
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish?: string
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -2000,56 +1747,7 @@ export type SessionMessageOutput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly error?:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly retry?: {
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
}
readonly error?: { readonly type: "unknown"; readonly message: string }
}
| {
readonly type: "compaction"
@@ -2144,27 +1842,9 @@ export type MessageListOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell"
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly metadata: { readonly [x: string]: JsonValue }
readonly time: {
readonly started: number | "Infinity" | "-Infinity" | "NaN"
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
}
}
readonly output?: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
readonly callID: string
readonly command: string
readonly output: string
}
| {
readonly id: string
@@ -2174,20 +1854,23 @@ export type MessageListOutput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string }
| { readonly type: "text"; readonly id: string; readonly text: string }
| {
readonly type: "reasoning"
readonly id: string
readonly text: string
readonly state?: { readonly [x: string]: JsonValue }
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
readonly type: "tool"
readonly id: string
readonly name: string
readonly executed?: boolean
readonly providerState?: { readonly [x: string]: JsonValue }
readonly providerResultState?: { readonly [x: string]: JsonValue }
readonly provider?: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
}
readonly state:
| { readonly status: "pending"; readonly input: string }
| {
@@ -2225,37 +1908,7 @@ export type MessageListOutput = {
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly structured: { readonly [x: string]: JsonValue }
readonly error:
| {
readonly type: "provider.rate-limit"
readonly message: string
readonly retryAfterMs?: number
}
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: JsonValue
}
readonly time: {
@@ -2267,7 +1920,7 @@ export type MessageListOutput = {
}
>
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish?: string
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -2275,56 +1928,7 @@ export type MessageListOutput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly error?:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly retry?: {
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
}
readonly error?: { readonly type: "unknown"; readonly message: string }
}
| {
readonly type: "compaction"
@@ -2779,7 +2383,7 @@ export type ServerMcpListOutput = {
readonly name: string
readonly status:
| { readonly status: "connected" }
| { readonly status: "pending" }
| { readonly status: "disconnected" }
| { readonly status: "disabled" }
| { readonly status: "failed"; readonly error: string }
| { readonly status: "needs_auth" }
@@ -4159,50 +3763,49 @@ export type SkillListOutput = {
export type EventSubscribeOutput =
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "models-dev.refreshed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "integration.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "integration.connection.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly integrationID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "catalog.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "agent.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.created"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4261,10 +3864,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4323,10 +3925,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.deleted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4385,10 +3986,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "message.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4490,19 +4090,17 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "message.removed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly messageID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "message.part.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4738,84 +4336,82 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "message.part.removed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly messageID: string; readonly partID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.agent.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.agent.switched"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly agent: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly agent: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.model.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.model.switched"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.moved"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.moved"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly location: { readonly directory: string; readonly workspaceID?: string }
readonly subpath?: string
readonly subdirectory?: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.renamed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.renamed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly title: string }
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly title: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.forked"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.prompt.promoted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.prompt.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.forked"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly inputID: string
readonly parentID: string
readonly messageID?: string
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.prompted"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly prompt: {
readonly text: string
readonly files?: ReadonlyArray<{
@@ -4835,83 +4431,67 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.succeeded"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.prompt.admitted"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly messageID: string
readonly prompt: {
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
readonly mime: string
readonly name?: string
readonly description?: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
}>
}
readonly delivery: "steer" | "queue"
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.interrupted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "user" | "shutdown" | "superseded" }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.context.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.synthetic"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.execution.settled"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly outcome: "success" | "failure" | "interrupted"
readonly error?: { readonly type: "unknown"; readonly message: string }
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.context.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly text: string
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.synthetic"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly text: string
readonly description?: string
readonly metadata?: { readonly [x: string]: unknown }
@@ -4919,73 +4499,53 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.skill.activated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly name: string; readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.shell.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.skill.activated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number
readonly metadata: { readonly [x: string]: unknown }
readonly time: { readonly started: number; readonly completed?: number }
}
readonly messageID: string
readonly name: string
readonly text: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.shell.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.shell.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number
readonly metadata: { readonly [x: string]: unknown }
readonly time: { readonly started: number; readonly completed?: number }
}
readonly output: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
readonly messageID: string
readonly callID: string
readonly command: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.shell.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly callID: string
readonly output: string
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.step.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly agent: string
@@ -4995,15 +4555,15 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.step.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish: string
readonly cost: number
readonly tokens: {
readonly input: number
@@ -5017,108 +4577,109 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.step.failed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly delta: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.text.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly state?: { readonly [x: string]: unknown }
readonly textID: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.delta"
readonly type: "session.next.text.delta"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly delta: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly textID: string
readonly delta: string
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.text.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly textID: string
readonly text: string
readonly state?: { readonly [x: string]: unknown }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.input.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.reasoning.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly reasoningID: string
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.reasoning.delta"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly reasoningID: string
readonly delta: string
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.reasoning.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly reasoningID: string
readonly text: string
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.tool.input.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
@@ -5127,11 +4688,12 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.input.delta"
readonly type: "session.next.tool.input.delta"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
@@ -5140,12 +4702,12 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.input.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.input.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
@@ -5154,28 +4716,31 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.called"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.called"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
readonly tool: string
readonly input: { readonly [x: string]: unknown }
readonly executed: boolean
readonly state?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.progress"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.progress"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
@@ -5188,12 +4753,12 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.success"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.success"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
@@ -5204,112 +4769,87 @@ export type EventSubscribeOutput =
>
readonly outputPaths?: ReadonlyArray<string>
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.tool.failed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.retry.scheduled"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.retried"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly assistantMessageID: string
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: {
readonly message: string
readonly statusCode?: number
readonly isRetryable: boolean
readonly responseHeaders?: { readonly [x: string]: string }
readonly responseBody?: string
readonly metadata?: { readonly [x: string]: string }
}
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.compaction.started"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly reason: "auto" | "manual"
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.compaction.delta"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly text: string
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.next.compaction.ended"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly messageID: string
readonly reason: "auto" | "manual"
readonly text: string
readonly recent: string
@@ -5317,12 +4857,12 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.staged"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.revert.staged"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly timestamp: number
readonly sessionID: string
readonly revert: {
readonly messageID: string
@@ -5341,43 +4881,41 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.cleared"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.revert.cleared"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
readonly data: { readonly timestamp: number; readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.committed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.next.revert.committed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly to: string }
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "filesystem.changed"
readonly type: "file.edited"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly file: string; readonly event: "add" | "change" | "unlink" }
readonly data: { readonly file: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "reference.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "permission.v2.asked"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly id: string
@@ -5391,9 +4929,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "permission.v2.replied"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -5403,49 +4941,49 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "plugin.added"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly id: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "project.directories.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly projectID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "command.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "config.updated"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "skill.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "file.watcher.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly file: string; readonly event: "add" | "change" | "unlink" }
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "pty.created"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly info: {
@@ -5462,9 +5000,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "pty.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly info: {
@@ -5481,25 +5019,25 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "pty.exited"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly id: string; readonly exitCode: number }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "pty.deleted"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly id: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "shell.created"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly info: {
@@ -5518,9 +5056,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "shell.exited"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly id: string
@@ -5530,17 +5068,17 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "shell.deleted"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly id: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "question.v2.asked"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly id: string
@@ -5557,9 +5095,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "question.v2.replied"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -5569,17 +5107,17 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "question.v2.rejected"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly requestID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "form.created"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly form:
@@ -5692,9 +5230,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "form.replied"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly id: string
@@ -5704,17 +5242,17 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "form.cancelled"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly id: string; readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "todo.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -5723,9 +5261,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.status"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -5750,25 +5288,25 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.idle"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tui.prompt.append"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tui.command.execute"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly command:
@@ -5794,9 +5332,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tui.toast.show"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly title?: string
@@ -5807,49 +5345,49 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tui.session.select"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "installation.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly version: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "installation.update-available"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly version: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "vcs.branch.updated"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly branch?: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "mcp.status.changed"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly server: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "permission.asked"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly id: string
@@ -5863,9 +5401,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "permission.replied"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -5875,9 +5413,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "question.asked"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly id: string
@@ -5894,9 +5432,9 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "question.replied"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -5906,17 +5444,17 @@ export type EventSubscribeOutput =
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "question.rejected"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly requestID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.error"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID?: string | undefined
@@ -5957,6 +5495,7 @@ export type EventSubscribeOutput =
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } | undefined
readonly location?: { readonly directory: string; readonly workspaceID?: string } | undefined
readonly type: "server.connected"
readonly data: {}
+1 -13
View File
@@ -1,16 +1,4 @@
export * from "./generated/index"
export type {
AgentApi,
CatalogApi,
CommandApi,
EventApi,
IntegrationApi,
ModelApi,
PluginApi,
ProviderApi,
ReferenceApi,
SessionApi,
SkillApi,
} from "./api.js"
export type { Transport } from "../effect/service.js"
export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types"
export type OpenCodeClient = ReturnType<typeof import("./generated/client").make>
-10
View File
@@ -1,10 +0,0 @@
import { Effect } from "effect"
import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect"
type EffectClient = Effect.Success<ReturnType<typeof EffectOpenCode.make>>
declare const effectClient: EffectClient
const effectApi: EffectApi<unknown> = effectClient
void effectApi
+10 -19
View File
@@ -1,17 +1,7 @@
import { expect, test } from "bun:test"
import { DateTime, Effect, Stream } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import {
AbsolutePath,
Agent,
Event,
Location,
Model,
OpenCode,
Prompt,
Session,
SessionMessage,
} from "../src/effect/index"
import { AbsolutePath, Agent, Event, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect/index"
const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) }
@@ -33,7 +23,7 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
HttpClientResponse.fromWeb(
request,
new Response(
`data: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` +
`data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
{ headers: { "content-type": "text/event-stream" } },
),
@@ -45,10 +35,10 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
return yield* client.event.subscribe().pipe(Stream.runCollect)
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.model.selected"])
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.next.model.switched"])
const durable = events[1]
if (durable?.type !== "session.model.selected") throw new Error("Expected model event")
expect(DateTime.toEpochMillis(durable.created)).toBe(1_717_171_717_000)
if (durable?.type !== "session.next.model.switched") throw new Error("Expected model event")
expect(DateTime.toEpochMillis(durable.data.timestamp)).toBe(1_717_171_717_000)
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
})
@@ -159,8 +149,8 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
expect(result.context).toEqual([])
expect(logQueries[0]).toEqual({ after: "0" })
const logged = Array.from(result.log)
expect(logged.map((item) => item.type)).toEqual(["session.model.selected", "log.synced"])
expect(logged[0]?.type === "session.model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe(
expect(logged.map((item) => item.type)).toEqual(["session.next.model.switched", "log.synced"])
expect(logged[0]?.type === "session.next.model.switched" && DateTime.toEpochMillis(logged[0].data.timestamp)).toBe(
1_717_171_717_000,
)
expect(logged.at(-1)).toEqual(synced)
@@ -227,11 +217,12 @@ const modelSwitchedMessage = {
const modelSwitchedEvent = {
id: "evt_model",
created: 1_717_171_717_000,
type: "session.model.selected",
type: "session.next.model.switched",
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
data: {
timestamp: 1_717_171_717_000,
sessionID: "ses_test",
messageID: "msg_model",
model: { id: "claude", providerID: "anthropic" },
},
}
+6 -5
View File
@@ -151,7 +151,7 @@ test("event.subscribe exposes the Promise event stream wire projection", async (
baseUrl: "http://localhost:3000",
fetch: async () =>
new Response(
`: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` +
`: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
{ headers: { "content-type": "text/event-stream" } },
),
@@ -159,8 +159,8 @@ test("event.subscribe exposes the Promise event stream wire projection", async (
const events = []
for await (const event of client.event.subscribe()) events.push(event)
expect(events).toEqual([{ id: "evt_connected", created: 0, type: "server.connected", data: {} }, modelSwitchedEvent])
expect(events[1]?.type === "session.model.selected" && events[1].created).toBe(1_717_171_717_000)
expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent])
expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000)
})
test("event.subscribe terminates on malformed Promise SSE data", async () => {
@@ -328,11 +328,12 @@ const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 }
const modelSwitchedEvent = {
id: "evt_model",
created: 1_717_171_717_000,
type: "session.model.selected",
type: "session.next.model.switched",
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
data: {
timestamp: 1_717_171_717_000,
sessionID: "ses_test",
messageID: "msg_model",
model: { id: "claude", providerID: "anthropic" },
},
}
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"noEmit": false,
"declaration": true
},
"include": ["src"]
}
-2
View File
@@ -3,8 +3,6 @@
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"allowImportingTsExtensions": false,
"allowJs": false,
"noUncheckedIndexedAccess": false
},
"include": ["src"]
+6 -7
View File
@@ -458,8 +458,8 @@ export const discoveryPlan = <R>(
// Section order is deliberate: workflow first (the top is the least likely part of a long
// description to be truncated or skimmed away), then rules, then syntax, with the budgeted
// catalog at the bottom. Example call forms use placeholders - never a real or fabricated
// tool name - and show both dot and bracket notation so non-identifier names are not normalized.
// catalog at the bottom. Example call forms use explicit `<namespace>.<tool>` placeholders -
// never a real or fabricated tool name.
const intro = [
"Write a CodeMode program to answer the request. Return code only.",
empty
@@ -467,7 +467,6 @@ export const discoveryPlan = <R>(
: complete
? "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed below; surrounding agent tools are not available unless listed here."
: "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed or searchable below; surrounding agent tools are not available unless listed here.",
...(empty ? [] : ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]),
]
// The search step exists only when search is advertised (PARTIAL catalog); a COMPLETE
@@ -481,14 +480,14 @@ export const discoveryPlan = <R>(
...(complete
? [
"1. Pick a tool from the list under `## Available tools` - each line is the exact call signature; use it as-is rather than guessing segments.",
'2. Call it using the exact signature shown; bracket notation and quotes are part of the path.',
"2. Call it using the exact signature shown: `const res = await tools.<namespace>.<tool>(input)` - bracket notation may appear for names that are not JavaScript identifiers.",
'3. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
"4. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
]
: [
'1. If the exact signature is not listed below, first search: `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })`.',
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
"2. Read the matches: each item is `{ path, description, signature }` - read the description before using an unfamiliar tool.",
"3. Call the result's `path` as-is; bracket notation and quotes are part of the path.",
"3. Call it with the result's `path` as-is (never guess segments): `const res = await tools.<namespace>.<tool>(input)` - bracket notation may appear for names that are not JavaScript identifiers.",
'4. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
"5. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
]),
@@ -505,7 +504,7 @@ export const discoveryPlan = <R>(
: "- Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`; tools from the surrounding agent/runtime are not implicitly exposed.",
"- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.",
"- A result typed `Promise<unknown>` has no guaranteed shape - verify what actually came back before relying on its fields.",
'- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`, or use `tools.<namespace>["tool-name"](item)` when the listed signature uses bracket notation.',
"- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`.",
"- `Object.keys(tools)` lists namespaces; `Object.keys(tools.<namespace>)` lists its tools; `for...in` works on both.",
...(complete
? []
+5 -5
View File
@@ -622,12 +622,12 @@ describe("CodeMode public contract", () => {
)
expect(instructions).toContain("Return only the fields you need")
expect(instructions).toContain("raw payloads get truncated and waste context")
expect(instructions).toContain("Do not infer or normalize tool names")
expect(instructions).toContain("bracket notation and quotes are part of the path")
expect(instructions).toContain("`const res = await tools.<namespace>.<tool>(input)`")
expect(instructions).toContain("surrounding agent tools are not available unless listed here")
expect(instructions).toContain("Only tools listed here are available inside `tools`")
// Placeholders use generic namespace/tool/field names only - no fabricated real tools
// and no real catalog tools cherry-picked into example lines.
expect(instructions).toContain("bracket notation may appear for names that are not JavaScript identifiers")
// Placeholders use the <namespace>.<tool>/<field> style ONLY - no fabricated tool
// names, and no real catalog tools cherry-picked into example lines.
expect(instructions).toContain("`return { <field>: data.<field> }`")
expect(instructions).not.toContain("total_count")
expect(instructions).not.toContain("list_issues")
@@ -640,7 +640,7 @@ describe("CodeMode public contract", () => {
// PARTIAL: the workflow starts with search (with query-style guidance that is clearly
// a query string, never a tool name) and the browse-namespace rule appears.
expect(partial).toContain(
'1. If the exact signature is not listed below, first search: `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })`.',
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
)
expect(partial).toContain(
"Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`",
-40
View File
@@ -339,43 +339,3 @@ describe("pretty signatures in search results", () => {
expect(instructions).not.toContain("/**")
})
})
describe("non-identifier tool paths", () => {
const resolveLibrary = Tool.make({
description: "Resolve a Context7 library ID",
input: {
type: "object",
properties: {
query: { type: "string" },
libraryName: { type: "string" },
},
required: ["query", "libraryName"],
} as const,
run: () => Effect.succeed("/reactjs/react.dev"),
})
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
test("inline catalog uses bracket notation for dashed tool names", () => {
const instructions = runtime.instructions()
expect(instructions).toContain(
'tools.context7["resolve-library-id"](input: { query: string; libraryName: string }): Promise<unknown>',
)
expect(instructions).toContain("Do not infer or normalize tool names")
expect(instructions).toContain("bracket notation and quotes are part of the path")
expect(instructions).not.toContain("tools.context7.resolve-library-id")
expect(instructions).not.toContain("tools.context7.resolve_library_id")
})
test("search results return callable bracket-notation paths and signatures", async () => {
const result = await Effect.runPromise(
runtime.execute(`return await tools.$codemode.search({ query: "resolve library" })`),
)
expect(result.ok).toBe(true)
if (!result.ok) throw new Error("search failed")
const value = result.value as { items: Array<{ path: string; signature: string }> }
expect(value.items[0]?.path).toBe('tools.context7["resolve-library-id"]')
expect(value.items[0]?.signature).toContain('tools.context7["resolve-library-id"](input: {')
})
})
+2 -12
View File
@@ -1,9 +1,9 @@
{
"version": "7",
"dialect": "sqlite",
"id": "96e9fe64-d810-4102-8f79-3317a88bb6d2",
"id": "22e57fed-b9b8-4e94-a3b4-f94bece680a8",
"prevIds": [
"22e57fed-b9b8-4e94-a3b4-f94bece680a8"
"f14a9b18-8207-487e-a3d3-227e629ba9ad"
],
"ddl": [
{
@@ -526,16 +526,6 @@
"entityType": "columns",
"table": "event"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "created",
"entityType": "columns",
"table": "event"
},
{
"type": "text",
"notNull": true,
+1 -1
View File
@@ -1,7 +1,7 @@
export * as AccountV2 from "./account"
import { Schema } from "effect"
import type { HttpClientError } from "effect/unstable/http"
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
export const ID = Schema.String.pipe(Schema.brand("AccountID"))
export type ID = Schema.Schema.Type<typeof ID>
+13 -2
View File
@@ -1,11 +1,12 @@
export * as Catalog from "./catalog"
import { makeLocationNode } from "./effect/app-node"
import { Array, Context, Effect, Layer, Option, Order, pipe } from "effect"
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
import { Catalog } from "@opencode-ai/schema/catalog"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import { EventV2 } from "./event"
import { Policy } from "./policy"
import { State } from "./state"
import { Integration } from "./integration"
@@ -16,6 +17,8 @@ export type ProviderRecord = {
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
export const PolicyActions = Schema.Literals(["provider.use"])
export const Event = Catalog.Event
type Data = {
@@ -62,6 +65,7 @@ const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const policy = yield* Policy.Service
const integrations = yield* Integration.Service
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
@@ -155,6 +159,13 @@ const layer = Layer.effect(
return result
},
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) {
if (policy.hasStatements()) {
for (const record of [...catalog.provider.list()]) {
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
catalog.provider.remove(record.provider.id)
}
}
}
yield* events.publish(Event.Updated, {})
}),
})
@@ -283,4 +294,4 @@ const layer = Layer.effect(
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Policy.node, Integration.node] })
+44 -80
View File
@@ -3,19 +3,18 @@ export * as Config from "./config"
import { makeLocationNode } from "./effect/app-node"
import path from "path"
import { type ParseError, parse } from "jsonc-parser"
import { Context, Effect, Fiber, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Context, Effect, Layer, Option, Schema } from "effect"
import { Permission } from "@opencode-ai/schema/permission"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { EventV2 } from "./event"
import { Watcher } from "./filesystem/watcher"
import { FSUtil } from "./fs-util"
import { Global } from "./global"
import { Location } from "./location"
import { Policy } from "./policy"
import { AbsolutePath } from "./schema"
import { ConfigAgent } from "./config/agent"
import { ConfigAttachments } from "./config/attachments"
import { ConfigCompaction } from "./config/compaction"
import { ConfigCommand } from "./config/command"
import { ConfigExperimental } from "./config/experimental"
import { ConfigFormatter } from "./config/formatter"
import { ConfigLSP } from "./config/lsp"
import { ConfigMCP } from "./config/mcp"
@@ -23,7 +22,6 @@ import { ConfigPlugin } from "./config/plugin"
import { ConfigProvider } from "./config/provider"
import { ConfigReference } from "./config/reference"
import { ConfigToolOutput } from "./config/tool-output"
import { ConfigVariable } from "./config/variable"
import { ConfigWatcher } from "./config/watcher"
import { ConfigV1 } from "./v1/config/config"
import { ConfigMigrateV1 } from "./v1/config/migrate"
@@ -104,6 +102,7 @@ export class Info extends Schema.Class<Info>("Config.Info")({
plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({
description: "Ordered external plugin packages to load",
}),
experimental: ConfigExperimental.Experimental.pipe(Schema.optional),
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
}) {}
@@ -139,8 +138,7 @@ const layer = Layer.effect(
const fs = yield* FSUtil.Service
const global = yield* Global.Service
const location = yield* Location.Service
const watcher = yield* Watcher.Service
const events = yield* EventV2.Service
const policy = yield* Policy.Service
const names = ["opencode.json", "opencode.jsonc"]
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
@@ -149,10 +147,9 @@ const layer = Layer.effect(
const loadFile = Effect.fnUntraced(function* (filepath: string) {
const text = yield* fs.readFileStringSafe(filepath)
if (!text) return
const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
const errors: ParseError[] = []
const input: unknown = parse(substituted, errors, { allowTrailingComma: true })
const input: unknown = parse(text, errors, { allowTrailingComma: true })
if (errors.length) return
const info = Option.getOrUndefined(
@@ -173,78 +170,45 @@ const layer = Layer.effect(
]
})
const discover = Effect.fn("Config.discover")(function* () {
const globalDirectory = AbsolutePath.make(global.config)
const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config)
const discovered = locationIsGlobal
? []
: yield* fs
.up({
targets: [".opencode", ...names.toReversed()],
start: location.directory,
stop: location.project.directory,
})
.pipe(Effect.orDie)
const directories = [
globalDirectory,
...discovered
.filter((item) => path.basename(item) === ".opencode")
.toReversed()
.map((directory) => AbsolutePath.make(directory)),
]
const directPaths = discovered.filter((item) => path.basename(item) !== ".opencode").toReversed()
const direct = yield* Effect.forEach(directPaths, loadFile).pipe(
Effect.orDie,
Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)),
)
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
return {
entries: [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()],
directories,
files: directPaths,
}
})
const initial = yield* discover()
let configs = initial.entries
const updates = yield* PubSub.unbounded<Watcher.Update>()
const subscriptions = new Map<string, Effect.Effect<unknown>>()
const targets = (snapshot: typeof initial) => [
...snapshot.directories.map((path) => ({ path, type: "directory" as const })),
...snapshot.files
.filter((file) => !snapshot.directories.some((directory) => FSUtil.contains(directory, file)))
.map((path) => ({ path, type: "file" as const })),
const globalDirectory = AbsolutePath.make(global.config)
const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config)
// Read configuration once when this location opens. Later calls reuse these
// values until the location is reopened.
const discovered = locationIsGlobal
? []
: yield* fs
.up({
targets: [".opencode", ...names.toReversed()],
start: location.directory,
stop: location.project.directory,
})
.pipe(Effect.orDie)
const directories = [
globalDirectory,
...discovered
.filter((item) => path.basename(item) === ".opencode")
.toReversed()
.map((directory) => AbsolutePath.make(directory)),
]
const reconcile = Effect.fn("Config.reconcileWatches")(function* (snapshot: typeof initial) {
const next = new Map(targets(snapshot).map((target) => [JSON.stringify(target), target]))
for (const [key, stop] of subscriptions) {
if (next.has(key)) continue
yield* stop
subscriptions.delete(key)
}
for (const [key, target] of next) {
if (subscriptions.has(key)) continue
const fiber = yield* watcher.subscribe(target).pipe(
Stream.runForEach((update) => PubSub.publish(updates, update)),
Effect.forkScoped({ startImmediately: true }),
)
subscriptions.set(key, Fiber.interrupt(fiber))
}
})
yield* Stream.fromPubSub(updates).pipe(
Stream.debounce("100 millis"),
Stream.runForEach((update) =>
Effect.gen(function* () {
const next = yield* discover()
configs = next.entries
yield* reconcile(next)
yield* events.publish(ConfigSchema.Event.Updated, {})
}).pipe(Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause }))),
),
Effect.forkScoped({ startImmediately: true }),
// A config closer to the opened directory should win over one higher up.
// Search starts nearby, so reverse the results before applying them.
const directPaths = discovered.filter((item) => path.basename(item) !== ".opencode").toReversed()
const direct = yield* Effect.forEach(directPaths, loadFile).pipe(
Effect.orDie,
Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)),
)
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
// Apply general settings first and more specific settings last:
// global config, project files, then `.opencode` files.
const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()]
// Rules use the opposite order so a user-global rule can override a
// repository rule. Statement order inside each file stays unchanged.
yield* policy.load(
configs
.filter((config): config is Document => config.type === "document")
.toReversed()
.flatMap((config) => config.info.experimental?.policies ?? []),
)
yield* reconcile(initial)
return Service.of({
entries: Effect.fn("Config.entries")(function* () {
@@ -257,5 +221,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node],
deps: [FSUtil.node, Global.node, Location.node, Policy.node],
})
+18
View File
@@ -0,0 +1,18 @@
export * as ConfigExperimental from "./experimental"
import { Schema } from "effect"
import { Catalog } from "../catalog"
import { Policy as PolicyV2 } from "../policy"
// Each core domain exports the policy actions it supports. Adding an action to
// this union makes it valid in authored config while keeping Policy generic.
export const PolicyAction = Schema.Union([Catalog.PolicyActions])
export class Policy extends Schema.Class<Policy>("ConfigV2.Experimental.Policy")({
...PolicyV2.Info.fields,
action: PolicyAction,
}) {}
export class Experimental extends Schema.Class<Experimental>("ConfigV2.Experimental")({
policies: Policy.pipe(Schema.Array, Schema.optional),
}) {}
+57 -68
View File
@@ -2,7 +2,7 @@ export * as ConfigAgentPlugin from "./agent"
import { define } from "../../plugin/internal"
import path from "path"
import { Effect, Option, Schema, Stream } from "effect"
import { Effect, Option, Schema } from "effect"
import { AgentV2 } from "../../agent"
import { Config } from "../../config"
import { ConfigAgent } from "../agent"
@@ -38,75 +38,64 @@ export const Plugin = define({
effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service
const fs = yield* FSUtil.Service
const load = Effect.fn("ConfigAgentPlugin.load")(function* () {
return yield* Effect.forEach(yield* config.entries(), (entry) => {
if (entry.type === "document") return Effect.succeed([entry])
return Effect.gen(function* () {
const files = yield* discover(fs, entry.path)
return yield* Effect.forEach(files, (file) =>
fs.readFileStringSafe(file.filepath).pipe(
Effect.map((content) => content && decode(file, content)),
Effect.catch(() => Effect.succeed(undefined)),
),
).pipe(
Effect.map((documents) =>
documents.filter((document): document is Config.Document => document !== undefined),
),
)
})
}).pipe(Effect.map((documents) => documents.flat()))
})
const loaded = { documents: yield* load() }
yield* ctx.agent.transform((draft) => {
const global = loaded.documents.flatMap((document) => document.info.permissions ?? [])
const configuredDefault = Config.latest(loaded.documents, "default_agent")
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
for (const current of draft.list()) {
draft.update(current.id, (agent) => agent.permissions.push(...global))
}
for (const document of loaded.documents) {
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
const agentID = AgentV2.ID.make(id)
if (item.disabled) {
draft.remove(agentID)
continue
}
const exists = draft.get(agentID) !== undefined
draft.update(agentID, (agent) => {
if (!exists) agent.permissions.push(...global)
if (item.model !== undefined) {
const model = ModelV2.parse(item.model)
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
}
if (item.variant !== undefined && agent.model !== undefined) {
agent.model.variant = ModelV2.VariantID.make(item.variant)
}
if (item.request !== undefined) {
Object.assign(agent.request.headers, item.request.headers ?? {})
Object.assign(agent.request.body, item.request.body ?? {})
}
if (item.system !== undefined) agent.system = item.system
if (item.description !== undefined) agent.description = item.description
if (item.mode !== undefined) agent.mode = item.mode
if (item.hidden !== undefined) agent.hidden = item.hidden
if (item.color !== undefined) agent.color = item.color
if (item.steps !== undefined) agent.steps = item.steps
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
yield* ctx.agent.transform(
Effect.fn(function* (draft) {
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
if (entry.type === "document") return Effect.succeed([entry])
return Effect.gen(function* () {
const files = yield* discover(fs, entry.path)
return yield* Effect.forEach(files, (file) =>
fs.readFileStringSafe(file.filepath).pipe(
Effect.map((content) => content && decode(file, content)),
Effect.catch(() => Effect.succeed(undefined)),
),
).pipe(
Effect.map((documents) =>
documents.filter((document): document is Config.Document => document !== undefined),
),
)
})
}).pipe(Effect.map((documents) => documents.flat()))
const global = documents.flatMap((document) => document.info.permissions ?? [])
const configuredDefault = Config.latest(documents, "default_agent")
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
for (const current of draft.list()) {
draft.update(current.id, (agent) => agent.permissions.push(...global))
}
}
})
yield* ctx.event.subscribe().pipe(
Stream.filter((event) => event.type === "config.updated"),
Stream.runForEach(() =>
load().pipe(
Effect.tap((documents) => Effect.sync(() => (loaded.documents = documents))),
Effect.andThen(ctx.agent.reload()),
),
),
Effect.forkScoped({ startImmediately: true }),
for (const document of documents) {
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
const agentID = AgentV2.ID.make(id)
if (item.disabled) {
draft.remove(agentID)
continue
}
const exists = draft.get(agentID) !== undefined
draft.update(agentID, (agent) => {
if (!exists) agent.permissions.push(...global)
if (item.model !== undefined) {
const model = ModelV2.parse(item.model)
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
}
if (item.variant !== undefined && agent.model !== undefined) {
agent.model.variant = ModelV2.VariantID.make(item.variant)
}
if (item.request !== undefined) {
Object.assign(agent.request.headers, item.request.headers ?? {})
Object.assign(agent.request.body, item.request.body ?? {})
}
if (item.system !== undefined) agent.system = item.system
if (item.description !== undefined) agent.description = item.description
if (item.mode !== undefined) agent.mode = item.mode
if (item.hidden !== undefined) agent.hidden = item.hidden
if (item.color !== undefined) agent.color = item.color
if (item.steps !== undefined) agent.steps = item.steps
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
})
}
}
}),
)
}),
})
+28 -39
View File
@@ -2,7 +2,7 @@ export * as ConfigCommandPlugin from "./command"
import { define } from "../../plugin/internal"
import path from "path"
import { Effect, Option, Schema, Stream } from "effect"
import { Effect, Option, Schema } from "effect"
import { CommandV2 } from "../../command"
import { Config } from "../../config"
import { FSUtil } from "../../fs-util"
@@ -17,45 +17,34 @@ export const Plugin = define({
effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service
const fs = yield* FSUtil.Service
const load = Effect.fn("ConfigCommandPlugin.load")(function* () {
return yield* Effect.forEach(yield* config.entries(), (entry) => {
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
return loadDirectory(fs, entry.path).pipe(
Effect.map((commands) => [
{ commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
]),
)
}).pipe(Effect.map((documents) => documents.flat()))
})
const loaded = { documents: yield* load() }
yield* ctx.command.transform((draft) => {
for (const document of loaded.documents) {
for (const [name, command] of Object.entries(document.commands ?? {})) {
draft.update(name, (item) => {
item.template = command.template
if (command.description !== undefined) item.description = command.description
if (command.agent !== undefined) item.agent = command.agent
if (command.model !== undefined) {
const model = ModelV2.parse(command.model)
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
}
if (command.variant !== undefined && item.model !== undefined) {
item.model.variant = ModelV2.VariantID.make(command.variant)
}
if (command.subtask !== undefined) item.subtask = command.subtask
})
yield* ctx.command.transform(
Effect.fn(function* (draft) {
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
return loadDirectory(fs, entry.path).pipe(
Effect.map((commands) => [
{ commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
]),
)
}).pipe(Effect.map((documents) => documents.flat()))
for (const document of documents) {
for (const [name, command] of Object.entries(document.commands ?? {})) {
draft.update(name, (item) => {
item.template = command.template
if (command.description !== undefined) item.description = command.description
if (command.agent !== undefined) item.agent = command.agent
if (command.model !== undefined) {
const model = ModelV2.parse(command.model)
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
}
if (command.variant !== undefined && item.model !== undefined) {
item.model.variant = ModelV2.VariantID.make(command.variant)
}
if (command.subtask !== undefined) item.subtask = command.subtask
})
}
}
}
})
yield* ctx.event.subscribe().pipe(
Stream.filter((event) => event.type === "config.updated"),
Stream.runForEach(() =>
load().pipe(
Effect.tap((documents) => Effect.sync(() => (loaded.documents = documents))),
Effect.andThen(ctx.command.reload()),
),
),
Effect.forkScoped({ startImmediately: true }),
}),
)
}),
})
+10 -44
View File
@@ -2,8 +2,7 @@ export * as ConfigExternalPlugin from "./external"
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
import { Effect, Schema, Stream } from "effect"
import { createRequire } from "node:module"
import { Effect, Schema } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
import { Config } from "../../config"
@@ -36,9 +35,6 @@ const PluginPackage = Schema.Struct({
module: Schema.optional(Schema.String),
})
let importGeneration = 0
const moduleCache = createRequire(import.meta.url).cache
export const Plugin = define({
id: "config-plugin",
effect: Effect.fn(function* (ctx) {
@@ -46,9 +42,8 @@ export const Plugin = define({
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const active = new Set<string>()
const load = Effect.fn("ConfigExternalPlugin.load")(function* () {
const configured: { package: string; options?: Record<string, unknown> }[] = []
yield* Effect.gen(function* () {
const configured: { package: string; options?: Record<string, any> }[] = []
for (const entry of yield* config.entries()) {
if (entry.type === "document") {
@@ -103,55 +98,26 @@ export const Plugin = define({
}
}
return yield* Effect.forEach(configured, (ref) =>
Effect.gen(function* () {
for (const ref of configured) {
yield* Effect.gen(function* () {
const entrypoint = path.isAbsolute(ref.package)
? pathToFileURL(ref.package).href
: (yield* npm.add(ref.package)).entrypoint
if (!entrypoint) return
yield* Effect.log({ msg: "loading plugin", id: ref.package, entrypoint })
const mod = yield* Effect.promise(() => import(cacheBust(entrypoint)))
const mod = yield* Effect.promise(() => import(entrypoint))
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
return {
yield* ctx.plugin.add({
id: plugin.id,
effect: (host: Parameters<typeof plugin.effect>[0]) =>
plugin.effect({ ...host, options: ref.options ?? {} }),
}
}).pipe(
Effect.catchCause((cause) =>
Effect.logError("failed to load plugin", { package: ref.package, cause }).pipe(Effect.as(undefined)),
),
),
).pipe(Effect.map((plugins) => plugins.filter((plugin) => plugin !== undefined)))
})
const reconcile = Effect.fn("ConfigExternalPlugin.reconcile")(function* () {
const plugins = yield* load()
const next = new Set(plugins.map((plugin) => plugin.id))
for (const id of active) {
if (!next.has(id)) yield* ctx.plugin.remove(id)
effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
})
}).pipe(Effect.ignoreCause)
}
for (const plugin of plugins) yield* ctx.plugin.add(plugin)
active.clear()
for (const id of next) active.add(id)
})
yield* reconcile()
yield* ctx.event.subscribe().pipe(
Stream.filter((event) => event.type === "config.updated"),
Stream.runForEach(() => reconcile()),
Effect.forkScoped({ startImmediately: true }),
)
}),
})
function cacheBust(entrypoint: string) {
const url = path.isAbsolute(entrypoint) ? pathToFileURL(entrypoint) : new URL(entrypoint)
if (url.protocol === "file:") delete moduleCache[fileURLToPath(url)]
url.searchParams.set("opencode-reload", String(++importGeneration))
return url.href
}
const resolvePackageEntrypoint = Effect.fnUntraced(function* (fs: FSUtil.Interface, directory: string) {
const pkg = yield* fs.readJson(path.join(directory, "package.json")).pipe(
Effect.flatMap(Schema.decodeUnknownEffect(PluginPackage)),
+98 -105
View File
@@ -1,7 +1,7 @@
export * as ConfigProviderPlugin from "./provider"
import { define } from "../../plugin/internal"
import { Effect, Stream } from "effect"
import { Effect } from "effect"
import { Config } from "../../config"
import { ModelV2 } from "../../model"
@@ -9,115 +9,108 @@ export const Plugin = define({
id: "config-provider",
effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service
const loaded = { entries: yield* config.entries() }
yield* ctx.integration.transform((integrations) => {
const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
const configuredIntegrations = new Set(
files.flatMap((file) =>
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) =>
provider.env === undefined ? [] : [id],
yield* ctx.integration.transform(
Effect.fn(function* (integrations) {
const files = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document")
const configuredIntegrations = new Set(
files.flatMap((file) =>
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) =>
provider.env === undefined ? [] : [id],
),
),
),
)
for (const file of files) {
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
const integrationID = id
if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
integrations.update(integrationID, (integration) => {
integration.name = item.name ?? integration.name
})
if (item.env !== undefined) {
integrations.method.update({
integrationID,
method: { type: "env", names: [...item.env] },
)
for (const file of files) {
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
const integrationID = id
if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
integrations.update(integrationID, (integration) => {
integration.name = item.name ?? integration.name
})
}
}
}
})
yield* ctx.catalog.transform((catalog) => {
const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
const configuredDefault = Config.latest(loaded.entries, "model")
if (configuredDefault !== undefined) {
const model = ModelV2.parse(configuredDefault)
catalog.model.default.set(model.providerID, model.modelID)
}
for (const file of files) {
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
const providerID = id
catalog.provider.update(providerID, (provider) => {
if (item.name !== undefined) provider.name = item.name
if (item.api !== undefined) provider.api = { ...item.api }
if (item.request !== undefined) {
Object.assign(provider.request.settings, item.request.settings)
Object.assign(provider.request.headers, item.request.headers)
Object.assign(provider.request.body, item.request.body)
if (item.env !== undefined) {
integrations.method.update({
integrationID,
method: { type: "env", names: [...item.env] },
})
}
})
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
input: [...config.capabilities.input],
output: [...config.capabilities.output],
}
}
if (config.request !== undefined) {
Object.assign(model.request.settings, config.request.settings)
Object.assign(model.request.headers, config.request.headers)
Object.assign(model.request.body, config.request.body)
if (config.request.variant !== undefined) model.request.variant = config.request.variant
}
if (config.variants !== undefined) {
for (const variant of config.variants) {
let existing = model.variants.find((item) => item.id === variant.id)
if (!existing) {
existing = {
id: variant.id,
settings: {},
headers: {},
body: {},
}
model.variants.push(existing)
}
Object.assign(existing.settings, variant.settings)
Object.assign(existing.headers, variant.headers)
Object.assign(existing.body, variant.body)
}
}
if (config.cost !== undefined) {
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
tier: cost.tier && { ...cost.tier },
input: cost.input,
output: cost.output,
cache: {
read: cost.cache?.read ?? 0,
write: cost.cache?.write ?? 0,
},
}))
}
if (config.disabled !== undefined) model.enabled = !config.disabled
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
})
}
}
}
})
yield* ctx.event.subscribe().pipe(
Stream.filter((event) => event.type === "config.updated"),
Stream.runForEach(() =>
config.entries().pipe(
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
Effect.andThen(ctx.integration.reload()),
Effect.andThen(ctx.catalog.reload()),
),
),
Effect.forkScoped({ startImmediately: true }),
}),
)
yield* ctx.catalog.transform(
Effect.fn(function* (catalog) {
const entries = yield* config.entries()
const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
const configuredDefault = Config.latest(entries, "model")
if (configuredDefault !== undefined) {
const model = ModelV2.parse(configuredDefault)
catalog.model.default.set(model.providerID, model.modelID)
}
for (const file of files) {
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
const providerID = id
catalog.provider.update(providerID, (provider) => {
if (item.name !== undefined) provider.name = item.name
if (item.api !== undefined) provider.api = { ...item.api }
if (item.request !== undefined) {
Object.assign(provider.request.settings, item.request.settings)
Object.assign(provider.request.headers, item.request.headers)
Object.assign(provider.request.body, item.request.body)
}
})
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
input: [...config.capabilities.input],
output: [...config.capabilities.output],
}
}
if (config.request !== undefined) {
Object.assign(model.request.settings, config.request.settings)
Object.assign(model.request.headers, config.request.headers)
Object.assign(model.request.body, config.request.body)
if (config.request.variant !== undefined) model.request.variant = config.request.variant
}
if (config.variants !== undefined) {
for (const variant of config.variants) {
let existing = model.variants.find((item) => item.id === variant.id)
if (!existing) {
existing = {
id: variant.id,
settings: {},
headers: {},
body: {},
}
model.variants.push(existing)
}
Object.assign(existing.settings, variant.settings)
Object.assign(existing.headers, variant.headers)
Object.assign(existing.body, variant.body)
}
}
if (config.cost !== undefined) {
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
tier: cost.tier && { ...cost.tier },
input: cost.input,
output: cost.output,
cache: {
read: cost.cache?.read ?? 0,
write: cost.cache?.write ?? 0,
},
}))
}
if (config.disabled !== undefined) model.enabled = !config.disabled
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
})
}
}
}
}),
)
}),
})
+34 -41
View File
@@ -2,7 +2,7 @@ export * as ConfigReferencePlugin from "./reference"
import { define } from "../../plugin/internal"
import path from "path"
import { Effect, Stream } from "effect"
import { Effect } from "effect"
import { Config } from "../../config"
import { ConfigReference } from "../reference"
import { Reference } from "../../reference"
@@ -16,47 +16,40 @@ export const Plugin = define({
const config = yield* Config.Service
const location = yield* Location.Service
const global = yield* Global.Service
const loaded = { entries: yield* config.entries() }
yield* ctx.reference.transform((draft) => {
const entries = new Map<string, Reference.Source>()
for (const doc of loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")) {
const directory = doc.path ? path.dirname(doc.path) : location.directory
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
if (!validAlias(name)) continue
const description = typeof entry === "string" ? undefined : entry.description
const hidden = typeof entry === "string" ? undefined : entry.hidden
entries.set(
name,
local(entry)
? Reference.LocalSource.make({
type: "local",
path: AbsolutePath.make(
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
),
...(description === undefined ? {} : { description }),
...(hidden === undefined ? {} : { hidden }),
})
: Reference.GitSource.make({
type: "git",
repository: typeof entry === "string" ? entry : entry.repository,
...(entry.branch === undefined ? {} : { branch: entry.branch }),
...(description === undefined ? {} : { description }),
...(hidden === undefined ? {} : { hidden }),
}),
)
yield* ctx.reference.transform(
Effect.fn(function* (draft) {
const entries = new Map<string, Reference.Source>()
for (const doc of (yield* config.entries()).filter(
(entry): entry is Config.Document => entry.type === "document",
)) {
const directory = doc.path ? path.dirname(doc.path) : location.directory
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
if (!validAlias(name)) continue
const description = typeof entry === "string" ? undefined : entry.description
const hidden = typeof entry === "string" ? undefined : entry.hidden
entries.set(
name,
local(entry)
? Reference.LocalSource.make({
type: "local",
path: AbsolutePath.make(
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
),
...(description === undefined ? {} : { description }),
...(hidden === undefined ? {} : { hidden }),
})
: Reference.GitSource.make({
type: "git",
repository: typeof entry === "string" ? entry : entry.repository,
...(entry.branch === undefined ? {} : { branch: entry.branch }),
...(description === undefined ? {} : { description }),
...(hidden === undefined ? {} : { hidden }),
}),
)
}
}
}
for (const [name, source] of entries) draft.add(name, source)
})
yield* ctx.event.subscribe().pipe(
Stream.filter((event) => event.type === "config.updated"),
Stream.runForEach(() =>
config.entries().pipe(
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
Effect.andThen(ctx.reference.reload()),
),
),
Effect.forkScoped({ startImmediately: true }),
for (const [name, source] of entries) draft.add(name, source)
}),
)
}),
})
+30 -38
View File
@@ -2,7 +2,7 @@ export * as ConfigSkillPlugin from "./skill"
import { define } from "../../plugin/internal"
import path from "path"
import { Effect, Stream } from "effect"
import { Effect } from "effect"
import { Config } from "../../config"
import { AbsolutePath } from "../../schema"
import { SkillV2 } from "../../skill"
@@ -15,44 +15,36 @@ export const Plugin = define({
const config = yield* Config.Service
const global = yield* Global.Service
const location = yield* Location.Service
const loaded = { entries: yield* config.entries() }
yield* ctx.skill.transform((draft) => {
const directories = loaded.entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
for (const directory of directories) {
draft.source(
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
)
draft.source(
SkillV2.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.join(directory, "skills")),
}),
)
}
for (const item of items) {
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
draft.source(SkillV2.UrlSource.make({ type: "url", url: item }))
continue
yield* ctx.skill.transform(
Effect.fn(function* (draft) {
const entries = yield* config.entries()
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
for (const directory of directories) {
draft.source(
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
)
draft.source(
SkillV2.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.join(directory, "skills")),
}),
)
}
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
draft.source(
SkillV2.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
}),
)
}
})
yield* ctx.event.subscribe().pipe(
Stream.filter((event) => event.type === "config.updated"),
Stream.runForEach(() =>
config.entries().pipe(
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
Effect.andThen(ctx.skill.reload()),
),
),
Effect.forkScoped({ startImmediately: true }),
for (const item of items) {
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
draft.source(SkillV2.UrlSource.make({ type: "url", url: item }))
continue
}
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
draft.source(
SkillV2.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
}),
)
}
}),
)
}),
})
-85
View File
@@ -1,85 +0,0 @@
export * as ConfigVariable from "./variable"
import os from "os"
import path from "path"
import { Effect } from "effect"
import { FSUtil } from "../fs-util"
import { InvalidError } from "../v1/config/error"
type ParseSource =
| {
type: "path"
path: string
}
| {
type: "virtual"
source: string
dir: string
}
type SubstituteInput = ParseSource & {
text: string
missing?: "error" | "empty"
env?: Record<string, string>
}
/** Apply {env:VAR} and {file:path} substitutions to config text. */
export const substitute = Effect.fn("ConfigVariable.substitute")(function* (input: SubstituteInput) {
const text = input.text.replace(
/\{env:([^}]+)\}/g,
(_, varName: string) => (input.env?.[varName] ?? process.env[varName]) || "",
)
if (!text.includes("{file:")) return text
return yield* substituteFiles(input, text)
})
const substituteFiles = Effect.fnUntraced(function* (input: SubstituteInput, text: string) {
const fs = yield* FSUtil.Service
const configDir = input.type === "path" ? path.dirname(input.path) : input.dir
const configSource = input.type === "path" ? input.path : input.source
const matches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
let out = ""
let cursor = 0
for (const match of matches) {
const token = match[0]
const index = match.index
out += text.slice(cursor, index)
const lineStart = text.lastIndexOf("\n", index - 1) + 1
const prefix = text.slice(lineStart, index).trimStart()
if (prefix.startsWith("//")) {
out += token
cursor = index + token.length
continue
}
const filePath = token.replace(/^\{file:/, "").replace(/\}$/, "")
const expandedPath = filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath
const resolvedPath = path.isAbsolute(expandedPath) ? expandedPath : path.resolve(configDir, expandedPath)
const fileContent = yield* fs.readFileString(resolvedPath).pipe(
Effect.catch((error) => {
if (input.missing === "empty") return Effect.succeed("")
const message = `bad file reference: "${token}"`
return Effect.fail(
new InvalidError(
{
path: configSource,
message:
error._tag === "PlatformError" && error.reason._tag === "NotFound"
? `${message} ${resolvedPath} does not exist`
: message,
},
{ cause: error },
),
)
}),
)
out += JSON.stringify(fileContent.trim()).slice(1, -1)
cursor = index + token.length
}
return out + text.slice(cursor)
})
@@ -106,7 +106,8 @@ const layer = Layer.effect(
yield* events.publish(SessionEvent.Moved, {
sessionID: input.sessionID,
location: Location.Ref.make({ directory }),
subpath: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")),
subdirectory: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")),
timestamp: yield* DateTime.now,
})
if (patch) {
+21 -12
View File
@@ -1,17 +1,26 @@
import type { NonEmptyReadonlyArray } from "effect/Array"
import { NodeFileSystem, NodePath, NodeSink, NodeStream } from "@effect/platform-node"
import { Deferred, Effect, Exit, FileSystem, Layer, Path, PlatformError, Predicate, Sink, Stream } from "effect"
import type { Scope } from "effect"
import { ChildProcess } from "effect/unstable/process"
import type * as Arr from "effect/Array"
import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node"
import * as NodePath from "@effect/platform-node/NodePath"
import * as Deferred from "effect/Deferred"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Path from "effect/Path"
import * as PlatformError from "effect/PlatformError"
import * as Predicate from "effect/Predicate"
import type * as Scope from "effect/Scope"
import * as Sink from "effect/Sink"
import * as Stream from "effect/Stream"
import * as ChildProcess from "effect/unstable/process/ChildProcess"
import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"
import {
ChildProcessSpawner,
ExitCode,
make,
make as makeSpawner,
makeHandle,
ProcessId,
type ChildProcessHandle,
} from "effect/unstable/process/ChildProcessSpawner"
// ast-grep-ignore: no-star-import
import * as NodeChildProcess from "node:child_process"
import { PassThrough } from "node:stream"
import launch from "cross-spawn"
@@ -62,7 +71,7 @@ const flatten = (command: ChildProcess.Command) => {
if (commands.length === 0) throw new Error("flatten produced empty commands array")
const [head, ...tail] = commands
return {
commands: [head, ...tail] as NonEmptyReadonlyArray<ChildProcess.StandardCommand>,
commands: [head, ...tail] as Arr.NonEmptyReadonlyArray<ChildProcess.StandardCommand>,
opts,
}
}
@@ -87,7 +96,7 @@ const toPlatformError = (
type ExitSignal = Deferred.Deferred<readonly [code: number | null, signal: NodeJS.Signals | null]>
const makeCrossSpawnSpawner = Effect.gen(function* () {
export const make = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
@@ -485,12 +494,12 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
},
)
return make(spawnCommand)
return makeSpawner(spawnCommand)
})
const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSystem | Path.Path> = Layer.effect(
ChildProcessSpawner,
makeCrossSpawnSpawner,
make,
)
export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] })
+3 -3
View File
@@ -1,7 +1,7 @@
export * as Database from "./database"
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
import { layer } from "#sqlite"
import { layer as sqliteLayer } from "#sqlite"
import { Context, Effect, Layer } from "effect"
import { Global } from "../global"
import { Flag } from "../flag/flag"
@@ -19,7 +19,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
const databaseLayer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const db = yield* makeDatabase
@@ -37,7 +37,7 @@ const databaseLayer = Layer.effect(
)
export function layerFromPath(filename: string) {
return databaseLayer.pipe(Layer.provide(layer({ filename })))
return layer.pipe(Layer.provide(sqliteLayer({ filename })))
}
export function path() {
-5
View File
@@ -41,10 +41,5 @@ export const migrations = (
import("./migration/20260622170816_reset_v2_session_state"),
import("./migration/20260622202450_simplify_session_input"),
import("./migration/20260702134641_add_session_context_entry"),
import("./migration/20260703090000_reset_v2_event_rename_sweep"),
import("./migration/20260703181610_event_created_column"),
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
import("./migration/20260703200000_reset_v2_event_fragments"),
import("./migration/20260703210000_reset_v2_execution_errors"),
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
+1 -1
View File
@@ -22,7 +22,7 @@ export function apply(db: Database) {
sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`,
)
if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations)
if (tables.length > 0) return yield* Effect.die(new Error("Database is not empty and has no session table"))
if (tables.length > 0) return yield* Effect.die("Database is not empty and has no session table")
yield* db.transaction((tx) =>
Effect.gen(function* () {
yield* schema.up(tx)
@@ -1,17 +0,0 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260703090000_reset_v2_event_rename_sweep",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`DELETE FROM \`session_input\`;`)
yield* tx.run(`DELETE FROM \`session_message\`;`)
yield* tx.run(`DELETE FROM \`event\`;`)
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
// `created` column is added by the generated 20260703181610_event_created_column
// migration, which runs after this wipe (NOT NULL without default is safe on the
// emptied table).
})
},
} satisfies DatabaseMigration.Migration
@@ -1,11 +0,0 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260703181610_event_created_column",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`event\` ADD \`created\` integer NOT NULL;`)
})
},
} satisfies DatabaseMigration.Migration
@@ -1,14 +0,0 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260703190000_reset_v2_shell_event_payloads",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`DELETE FROM \`session_input\`;`)
yield* tx.run(`DELETE FROM \`session_message\`;`)
yield* tx.run(`DELETE FROM \`event\`;`)
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
})
},
} satisfies DatabaseMigration.Migration
@@ -1,14 +0,0 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260703200000_reset_v2_event_fragments",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`DELETE FROM \`session_input\`;`)
yield* tx.run(`DELETE FROM \`session_message\`;`)
yield* tx.run(`DELETE FROM \`event\`;`)
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
})
},
} satisfies DatabaseMigration.Migration
@@ -1,14 +0,0 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260703210000_reset_v2_execution_errors",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`DELETE FROM \`session_input\`;`)
yield* tx.run(`DELETE FROM \`session_message\`;`)
yield* tx.run(`DELETE FROM \`event\`;`)
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
})
},
} satisfies DatabaseMigration.Migration
+1 -4
View File
@@ -1,6 +1,5 @@
import nodePath from "path"
import { customType } from "drizzle-orm/sqlite-core"
import { Schema } from "effect"
import { AbsolutePath } from "../schema"
function storagePath(input: string) {
@@ -75,8 +74,6 @@ export const pathColumn = customType<{
},
})
const decodeAbsoluteArray = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Array(Schema.String)))
export const absoluteArrayColumn = customType<{
data: AbsolutePath[]
driverData: string
@@ -89,6 +86,6 @@ export const absoluteArrayColumn = customType<{
return JSON.stringify(input.map(absolute))
},
fromDriver(input) {
return decodeAbsoluteArray(input).map((item) => AbsolutePath.make(toPlatform(absolute(item))))
return (JSON.parse(input) as string[]).map((item) => AbsolutePath.make(toPlatform(absolute(item))))
},
})
-1
View File
@@ -81,7 +81,6 @@ export default {
\`id\` text PRIMARY KEY,
\`aggregate_id\` text NOT NULL,
\`seq\` integer NOT NULL,
\`created\` integer NOT NULL,
\`type\` text NOT NULL,
\`data\` text NOT NULL,
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
+15 -8
View File
@@ -1,11 +1,18 @@
import { Database } from "bun:sqlite"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { Context, Effect, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Fiber from "effect/Fiber"
import { identity } from "effect/Function"
import { Reactivity } from "effect/unstable/reactivity"
import { SqlClient, Statement } from "effect/unstable/sql"
import * as Layer from "effect/Layer"
import * as Scope from "effect/Scope"
import * as Semaphore from "effect/Semaphore"
import * as Stream from "effect/Stream"
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
import * as Client from "effect/unstable/sql/SqlClient"
import type { Connection } from "effect/unstable/sql/SqlConnection"
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
import * as Statement from "effect/unstable/sql/Statement"
import { Sqlite } from "./sqlite"
const ATTR_DB_SYSTEM_NAME = "db.system.name"
@@ -13,7 +20,7 @@ const ATTR_DB_SYSTEM_NAME = "db.system.name"
const TypeId = "~@opencode-ai/core/database/SqliteBun" as const
type TypeId = typeof TypeId
interface SqliteClient extends SqlClient.SqlClient {
interface SqliteClient extends Client.SqlClient {
readonly [TypeId]: TypeId
readonly config: Config
readonly export: Effect.Effect<Uint8Array, SqlError>
@@ -50,7 +57,7 @@ const make = (options: Config) =>
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
const statement = native.query(query)
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers))
statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers))
try {
return Effect.succeed((statement.all(...(params as any)) ?? []) as Array<Record<string, unknown>>)
} catch (cause) {
@@ -66,7 +73,7 @@ const make = (options: Config) =>
Effect.withFiber<Array<unknown[]>, SqlError>((fiber) => {
const statement = native.query(query)
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers))
statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers))
try {
return Effect.succeed((statement.values(...(params as any)) ?? []) as Array<unknown[]>)
} catch (cause) {
@@ -123,7 +130,7 @@ const make = (options: Config) =>
})
const client = Object.assign(
(yield* SqlClient.make({
(yield* Client.make({
acquirer,
compiler,
transactionAcquirer,
@@ -159,7 +166,7 @@ const nativeLayer = (config: Config) =>
}),
)
const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config))
const drizzleLayer = Layer.effect(
Sqlite.Drizzle,
+15 -8
View File
@@ -1,11 +1,18 @@
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
import { drizzle } from "drizzle-orm/node-sqlite"
import { Context, Effect, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Fiber from "effect/Fiber"
import { identity } from "effect/Function"
import { Reactivity } from "effect/unstable/reactivity"
import { SqlClient, Statement } from "effect/unstable/sql"
import * as Layer from "effect/Layer"
import * as Scope from "effect/Scope"
import * as Semaphore from "effect/Semaphore"
import * as Stream from "effect/Stream"
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
import * as Client from "effect/unstable/sql/SqlClient"
import type { Connection } from "effect/unstable/sql/SqlConnection"
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
import * as Statement from "effect/unstable/sql/Statement"
import { Sqlite } from "./sqlite"
const ATTR_DB_SYSTEM_NAME = "db.system.name"
@@ -13,7 +20,7 @@ const ATTR_DB_SYSTEM_NAME = "db.system.name"
const TypeId = "~@opencode-ai/core/database/SqliteNode" as const
type TypeId = typeof TypeId
interface SqliteClient extends SqlClient.SqlClient {
interface SqliteClient extends Client.SqlClient {
readonly [TypeId]: TypeId
readonly config: Config
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
@@ -49,7 +56,7 @@ const make = (options: Config) =>
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
const statement = native.prepare(query)
statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers))
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
try {
return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array<Record<string, unknown>>)
} catch (cause) {
@@ -64,7 +71,7 @@ const make = (options: Config) =>
const runValues = (query: string, params: ReadonlyArray<unknown> = []) =>
Effect.withFiber<ReadonlyArray<ReadonlyArray<unknown>>, SqlError>((fiber) => {
const statement = native.prepare(query)
statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers))
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
statement.setReturnArrays(true)
try {
return Effect.succeed(
@@ -117,7 +124,7 @@ const make = (options: Config) =>
})
const client = Object.assign(
(yield* SqlClient.make({
(yield* Client.make({
acquirer,
compiler,
transactionAcquirer,
@@ -154,7 +161,7 @@ const nativeLayer = (config: Config) =>
}),
)
const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config))
const drizzleLayer = Layer.effect(
Sqlite.Drizzle,
+1 -1
View File
@@ -227,7 +227,7 @@ export function hoist<A, E, T extends Tag, const Items extends Replacements = re
}
if (node.tag === tag) {
const existing = hoisted.get(node.name)
if (existing && existing.implementation !== node.implementation) {
if (existing && existing !== node) {
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
}
hoisted.set(node.name, rewriteReplacementDependencies(node, replacementMap))
+2 -10
View File
@@ -1,6 +1,6 @@
export * as EventV2 from "./event"
import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
import { Event } from "@opencode-ai/schema/event"
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
import type { EventLog } from "@opencode-ai/schema/event-log"
@@ -55,7 +55,6 @@ export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* (
export type SerializedEvent = {
readonly id: ID
readonly type: string
readonly created?: DateTime.Utc
readonly seq: number
readonly aggregateID: string
readonly data: Record<string, unknown>
@@ -82,7 +81,6 @@ const decodeSerializedEvent = (event: SerializedEvent): Payload => {
}
return {
id: event.id,
created: event.created ?? DateTime.makeUnsafe(0),
type: definition.type,
durable: envelope(event.aggregateID, event.seq, definition.durable.version),
data: Schema.decodeUnknownSync(definition.data)(event.data),
@@ -94,9 +92,8 @@ export class SubscriberOverflowError extends Schema.TaggedErrorClass<SubscriberO
{ capacity: Schema.Int },
) {}
export const define = Event.define
export const versionedType = Event.versionedType
export const durable = Event.durable
export const ephemeral = Event.ephemeral
export interface PublishOptions {
readonly id?: ID
@@ -297,7 +294,6 @@ export const layerWith = (options?: LayerOptions) =>
if (
stored?.id === event.id &&
stored.type === versionedType(definition.type, durable.version) &&
stored.created === DateTime.toEpochMillis(event.created ?? DateTime.makeUnsafe(0)) &&
isDeepStrictEqual(stored.data, encoded)
) {
if (input.ownerID && row?.ownerID == null) {
@@ -369,7 +365,6 @@ export const layerWith = (options?: LayerOptions) =>
id: event.id,
aggregate_id: aggregateID,
seq,
created: DateTime.toEpochMillis(event.created ?? DateTime.makeUnsafe(0)),
type: versionedType(definition.type, durable.version),
data: encoded,
},
@@ -475,7 +470,6 @@ export const layerWith = (options?: LayerOptions) =>
definition,
{
id: options?.id ?? ID.create(),
created: yield* DateTime.now,
...(options?.metadata ? { metadata: options.metadata } : {}),
type: definition.type,
...(location ? { location } : {}),
@@ -499,7 +493,6 @@ export const layerWith = (options?: LayerOptions) =>
} else {
const payload = {
id: event.id,
created: event.created ?? DateTime.makeUnsafe(0),
type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data),
} as Payload
@@ -616,7 +609,6 @@ export const layerWith = (options?: LayerOptions) =>
return [
decodeSerializedEvent({
id: event.id,
created: DateTime.makeUnsafe(event.created),
aggregateID: event.aggregate_id,
seq: event.seq,
type: event.type,
-1
View File
@@ -15,7 +15,6 @@ export const EventTable = sqliteTable(
.notNull()
.references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }),
seq: integer().notNull(),
created: integer().notNull(),
type: text().notNull(),
data: text({ mode: "json" }).$type<Record<string, unknown>>().notNull(),
},
@@ -1,83 +0,0 @@
export * as LocationWatcher from "./location-watcher"
import { makeLocationNode } from "../effect/app-node"
import { Context, Effect, Layer, Stream } from "effect"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import os from "os"
import path from "path"
import { Config } from "../config"
import { EventV2 } from "../event"
import { FSUtil } from "../fs-util"
import { Git } from "../git"
import { Location } from "../location"
import { Watcher } from "./watcher"
import { Ignore } from "./ignore"
import { Protected } from "./protected"
function protecteds(dir: string) {
return Protected.paths().filter((item) => {
const relative = path.relative(dir, item)
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
})
}
export interface Interface {}
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationWatcher") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const location = yield* Location.Service
const watcher = yield* Watcher.Service
const events = yield* EventV2.Service
const fs = yield* FSUtil.Service
const git = yield* Git.Service
const configService = yield* Config.Service
const config = (yield* configService.entries())
.filter((entry): entry is Config.Document => entry.type === "document")
.flatMap((item) => item.info.watcher?.ignore ?? [])
const publish = (update: { type: "create" | "update" | "delete"; path: string }) =>
events.publish(FileSystem.Event.Changed, {
file: update.path,
event: update.type === "create" ? "add" : update.type === "update" ? "change" : "unlink",
})
if (path.resolve(location.directory) !== path.resolve(os.homedir())) {
yield* watcher
.subscribe({
path: location.directory,
type: "directory",
ignore: [...Ignore.PATTERNS, ...config, ...protecteds(location.directory)],
})
.pipe(Stream.runForEach(publish), Effect.forkScoped({ startImmediately: true }))
} else {
yield* Effect.logInfo("location watcher skipped home directory", { directory: location.directory })
}
if (location.vcs?.type === "git") {
const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory
const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined
if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) {
const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap(
(entry) => (entry.name === "HEAD" ? [] : [entry.name]),
)
yield* watcher
.subscribe({ path: vcs, type: "directory", ignore })
.pipe(Stream.runForEach(publish), Effect.forkScoped({ startImmediately: true }))
}
}
return Service.of({})
}).pipe(
Effect.catchCause((cause) =>
Effect.logError("failed to init location watcher service", { cause }).pipe(Effect.as(Service.of({}))),
),
),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, EventV2.node],
})
+102 -123
View File
@@ -3,20 +3,26 @@ export * as Watcher from "./watcher"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper"
import type ParcelWatcher from "@parcel/watcher"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { makeGlobalNode } from "../effect/app-node"
import { Cause, Context, Effect, Layer, PubSub, Scope, Stream } from "effect"
import { KeyedMutex } from "../effect/keyed-mutex"
import { Flag } from "../flag/flag"
import { lazy } from "../util/lazy"
import { watch as watchFileSystem } from "node:fs"
import { makeLocationNode } from "../effect/app-node"
import { Cause, Context, Effect, Layer } from "effect"
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
import os from "os"
import path from "path"
import { Config } from "../config"
import { EventV2 } from "../event"
import { Flag } from "../flag/flag"
import { FSUtil } from "../fs-util"
import { Git } from "../git"
import { Location } from "../location"
import { lazy } from "../util/lazy"
import { Ignore } from "./ignore"
import { Protected } from "./protected"
declare const OPENCODE_LIBC: string | undefined
const SUBSCRIBE_TIMEOUT_MS = 10_000
export const Event = { Updated: FileSystem.Event.Changed }
export const Event = FileSystemWatcher.Event
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try {
@@ -36,134 +42,107 @@ function getBackend() {
if (process.platform === "linux") return "inotify"
}
export const hasNativeBinding = () => !!watcher()
export type Update = ParcelWatcher.Event
export type WatchInput =
| { readonly path: string; readonly type: "file" }
| { readonly path: string; readonly type: "directory"; readonly ignore?: readonly string[] }
export interface Interface {
readonly subscribe: (input: WatchInput) => Stream.Stream<Update>
function protecteds(dir: string) {
return Protected.paths().filter((item) => {
const relative = path.relative(dir, item)
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
})
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Watcher") {}
export const hasNativeBinding = () => !!watcher()
export interface Interface {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileWatcher") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
if (Flag.OPENCODE_DISABLE_FILEWATCHER) return Service.of({})
const backend = getBackend()
const native = watcher()
if (Flag.OPENCODE_DISABLE_FILEWATCHER) {
return Service.of({ subscribe: () => Stream.empty })
const location = yield* Location.Service
if (path.resolve(location.directory) === path.resolve(os.homedir())) {
yield* Effect.logInfo("watcher skipped home directory", { directory: location.directory })
return Service.of({})
}
if (!backend) {
yield* Effect.logError("watcher backend not supported", {
directory: location.directory,
platform: process.platform,
})
return Service.of({})
}
type Entry = {
readonly pubsub: PubSub.PubSub<Update>
readonly subscription: { readonly unsubscribe: () => Promise<void> }
refs: number
}
const entries = new Map<string, Entry>()
const locks = KeyedMutex.makeUnsafe<string>()
const w = watcher()
if (!w) return Service.of({})
const acquire = Effect.fn("Watcher.acquire")(function* (input: WatchInput) {
const scope = yield* Scope.Scope
const target = path.resolve(input.path)
const directory = input.type === "file" ? path.dirname(target) : target
const ignore = [...new Set(input.type === "directory" ? (input.ignore ?? []) : [])].toSorted()
const id = JSON.stringify([input.type, target, ignore])
const pubsub = yield* locks.withLock(id)(
Effect.gen(function* () {
const existing = entries.get(id)
if (existing) {
existing.refs++
return existing.pubsub
}
const pubsub = yield* PubSub.unbounded<Update>()
const subscription = yield* input.type === "file"
? Effect.sync(() => {
const subscription = watchFileSystem(directory, { recursive: false }, (_event, file) => {
if (file && path.resolve(directory, file.toString()) !== target) return
PubSub.publishUnsafe(pubsub, {
path: target,
type: "update",
} satisfies Update)
})
if ("on" in subscription && typeof subscription.on === "function") {
subscription.on("error", (error: unknown) =>
Effect.runFork(Effect.logError("watcher callback failed", { path: target, error })),
)
}
return { unsubscribe: () => Promise.resolve(subscription.close()) }
})
: subscribeDirectory(native, backend, directory, ignore, pubsub)
if (subscription) {
entries.set(id, { pubsub, subscription, refs: 1 })
yield* Effect.logInfo("watcher started", {
path: target,
type: input.type,
backend: input.type === "file" ? "node" : backend,
ignores: ignore.length,
})
return pubsub
}
yield* PubSub.shutdown(pubsub)
return pubsub
yield* Effect.logInfo("watcher backend", { directory: location.directory, platform: process.platform, backend })
const events = yield* EventV2.Service
const fs = yield* FSUtil.Service
const git = yield* Git.Service
const context = yield* Effect.context()
const runFork = Effect.runForkWith(context)
const subscriptions: ParcelWatcher.AsyncSubscription[] = []
yield* Effect.addFinalizer(() =>
Effect.promise(() => Promise.allSettled(subscriptions.map((subscription) => subscription.unsubscribe()))),
)
const callback: ParcelWatcher.SubscribeCallback = (_error, updates) => {
if (_error) runFork(Effect.logError("watcher callback failed", { error: _error }))
for (const update of updates) {
if (update.type === "create") runFork(events.publish(Event.Updated, { file: update.path, event: "add" }))
if (update.type === "update") runFork(events.publish(Event.Updated, { file: update.path, event: "change" }))
if (update.type === "delete") runFork(events.publish(Event.Updated, { file: update.path, event: "unlink" }))
}
}
const subscribe = (directory: string, ignore: string[]) => {
const pending = w.subscribe(directory, callback, { ignore, backend })
return Effect.promise(() => pending).pipe(
Effect.tap((subscription) =>
Effect.sync(() => subscriptions.push(subscription)).pipe(
Effect.andThen(Effect.logInfo("watcher subscribed", { directory, backend, ignores: ignore.length })),
),
),
Effect.timeout(SUBSCRIBE_TIMEOUT_MS),
Effect.catchCause((cause) => {
pending.then((subscription) => subscription.unsubscribe()).catch(() => {})
return Effect.logError("failed to subscribe", { directory, cause: Cause.pretty(cause) })
}),
)
}
yield* Scope.addFinalizer(
scope,
locks.withLock(id)(
Effect.gen(function* () {
const entry = entries.get(id)
if (!entry) return
entry.refs--
if (entry.refs > 0) return
entries.delete(id)
yield* Effect.promise(() => entry.subscription.unsubscribe()).pipe(Effect.ignore)
yield* PubSub.shutdown(entry.pubsub)
yield* Effect.logInfo("watcher stopped", { path: target, type: input.type })
}),
),
const config = (yield* (yield* Config.Service).entries())
.filter((entry): entry is Config.Document => entry.type === "document")
.flatMap((item) => item.info.watcher?.ignore ?? [])
yield* Effect.forkScoped(
subscribe(location.directory, [...Ignore.PATTERNS, ...config, ...protecteds(location.directory)]),
)
if (location.vcs?.type === "git") {
const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory
const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined
if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) {
const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap(
(entry) => (entry.name === "HEAD" ? [] : [entry.name]),
)
yield* Effect.forkScoped(subscribe(vcs, ignore))
}
}
return Service.of({})
}).pipe(
Effect.catchCause((cause) => {
return Effect.logError("failed to init watcher service", { cause: Cause.pretty(cause) }).pipe(
Effect.as(Service.of({})),
)
return pubsub
})
const subscribe = (input: WatchInput) =>
Stream.unwrap(acquire(input).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub))))
return Service.of({ subscribe })
}),
}),
),
)
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
function subscribeDirectory(
native: typeof import("@parcel/watcher") | undefined,
backend: ParcelWatcher.BackendType | undefined,
directory: string,
ignore: string[],
pubsub: PubSub.PubSub<Update>,
) {
if (!native || !backend) {
return Effect.logError("watcher backend not supported", { directory, platform: process.platform }).pipe(
Effect.as(undefined),
)
}
const callback: ParcelWatcher.SubscribeCallback = (error, updates) => {
if (error) Effect.runFork(Effect.logError("watcher callback failed", { error }))
for (const update of updates) PubSub.publishUnsafe(pubsub, update)
}
const pending = native.subscribe(directory, callback, { ignore, backend })
return Effect.promise(() => pending).pipe(
Effect.timeout(SUBSCRIBE_TIMEOUT_MS),
Effect.catchCause((cause) => {
pending.then((subscription) => subscription.unsubscribe()).catch(() => {})
return Effect.logError("failed to subscribe", {
directory,
cause: Cause.pretty(cause),
}).pipe(Effect.as(undefined))
}),
)
}
export const node = makeLocationNode({
service: Service,
layer,
deps: [FSUtil.node, Location.node, Config.node, Git.node, EventV2.node],
})
+9 -20
View File
@@ -98,14 +98,10 @@ export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const forms = yield* Cache.makeWith<ID, Entry>(
() => Effect.die(new Error("Form cache must be used via set/getSuccess, never get")),
{
capacity: Number.MAX_SAFE_INTEGER,
timeToLive: (exit) =>
Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION,
},
)
const forms = yield* Cache.makeWith<ID, Entry>(() => Effect.die("Form cache must be used via set/getSuccess, never get"), {
capacity: Number.MAX_SAFE_INTEGER,
timeToLive: (exit) => (Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION),
})
const find = Effect.fn("Form.find")(function* (id: ID) {
return yield* Cache.getSuccess(forms, id).pipe(
@@ -135,9 +131,7 @@ export const layer = Layer.effect(
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
}
const form: Info =
input.mode === "form"
? { ...base, mode: "form", fields: input.fields }
: { ...base, mode: "url", url: input.url }
input.mode === "form" ? { ...base, mode: "form", fields: input.fields } : { ...base, mode: "url", url: input.url }
const entry: Entry = {
form,
state: { status: "pending" },
@@ -155,9 +149,7 @@ export const layer = Layer.effect(
Effect.gen(function* () {
const form = yield* create(input)
const entry = yield* find(form.id).pipe(Effect.orDie)
return yield* restore(Deferred.await(entry.deferred)).pipe(
Effect.onInterrupt(() => Effect.ignore(cancel(form.id))),
)
return yield* restore(Deferred.await(entry.deferred)).pipe(Effect.onInterrupt(() => Effect.ignore(cancel(form.id))))
}),
),
)
@@ -309,8 +301,7 @@ function validateField(field: Form.Field, value: Form.Value): string | undefined
return `Form field has invalid pattern: ${field.key}`
}
}
if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
return `Expected email for form field: ${field.key}`
if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return `Expected email for form field: ${field.key}`
if (field.format === "uri" && !isUri(value)) return `Expected URI for form field: ${field.key}`
if (field.format === "date" && !isDate(value)) return `Expected date for form field: ${field.key}`
if (field.format === "date-time" && !isDateTime(value)) return `Expected date-time for form field: ${field.key}`
@@ -333,10 +324,8 @@ function validateField(field: Form.Field, value: Form.Value): string | undefined
if (field.type === "multiselect") {
if (!isStringArray(value)) return `Expected string array for form field: ${field.key}`
if (field.required && value.length === 0) return `Missing required form field: ${field.key}`
if (field.minItems !== undefined && value.length < field.minItems)
return `Too few selections for form field: ${field.key}`
if (field.maxItems !== undefined && value.length > field.maxItems)
return `Too many selections for form field: ${field.key}`
if (field.minItems !== undefined && value.length < field.minItems) return `Too few selections for form field: ${field.key}`
if (field.maxItems !== undefined && value.length > field.maxItems) return `Too many selections for form field: ${field.key}`
if (!field.custom && value.some((item) => !field.options.some((option) => option.value === item))) {
return `Invalid option for form field: ${field.key}`
}
+6 -18
View File
@@ -1,7 +1,7 @@
import { NodeFileSystem } from "@effect/platform-node"
import path, { dirname, isAbsolute, join, relative, sep } from "path"
import { dirname, isAbsolute, join, relative, resolve as pathResolve, sep } from "path"
import { realpathSync } from "fs"
import { readdir } from "fs/promises"
import * as NFS from "fs/promises"
import { lookup } from "mime-types"
import { Context, Effect, FileSystem, Layer, Schema } from "effect"
import type { PlatformError } from "effect/PlatformError"
@@ -38,7 +38,6 @@ export namespace FSUtil {
readonly ensureDir: (path: string) => Effect.Effect<void, Error>
readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>
readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
readonly resolve: (path: string) => Effect.Effect<string>
readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect<string[], Error>
readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
@@ -50,9 +49,7 @@ export namespace FSUtil {
export const use = serviceUse(Service)
// Exported so simulation can wrap this layer and override the methods that
// bypass the injected FileSystem (readDirectoryEntries, glob, globUp).
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
@@ -80,7 +77,7 @@ export namespace FSUtil {
const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) {
return yield* Effect.tryPromise({
try: async () => {
const entries = await readdir(dirPath, { withFileTypes: true })
const entries = await NFS.readdir(dirPath, { withFileTypes: true })
return entries.map(
(e): DirEntry => ({
name: e.name,
@@ -92,14 +89,6 @@ export namespace FSUtil {
})
})
const resolve = Effect.fn("FileSystem.resolve")(function* (input: string) {
const resolved = path.resolve(windowsPath(input))
return yield* fs.realPath(resolved).pipe(
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(resolved)),
Effect.orDie,
)
})
const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
const text = yield* fs.readFileString(path)
return yield* Effect.try({
@@ -198,7 +187,6 @@ export namespace FSUtil {
isDir,
isFile,
readDirectoryEntries,
resolve,
readJson,
writeJson,
ensureDir,
@@ -221,7 +209,7 @@ export namespace FSUtil {
export function normalizePath(p: string): string {
if (process.platform !== "win32") return p
const resolved = path.resolve(windowsPath(p))
const resolved = pathResolve(windowsPath(p))
try {
return realpathSync.native(resolved)
} catch {
@@ -239,7 +227,7 @@ export namespace FSUtil {
}
export function resolve(p: string): string {
const resolved = path.resolve(windowsPath(p))
const resolved = pathResolve(windowsPath(p))
try {
return normalizePath(realpathSync(resolved))
} catch (e: any) {
+4 -6
View File
@@ -1,4 +1,4 @@
import { create } from "@opencode-ai/schema/identifier"
import { create as createIdentifier } from "@opencode-ai/schema/identifier"
const prefixes = {
job: "job",
@@ -23,7 +23,7 @@ export function descending(prefix: keyof typeof prefixes, given?: string) {
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
if (!given) {
return createID(prefixes[prefix], direction)
return create(prefixes[prefix], direction)
}
if (!given.startsWith(prefixes[prefix])) {
@@ -32,12 +32,10 @@ function generateID(prefix: keyof typeof prefixes, direction: "descending" | "as
return given
}
function createID(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
return prefix + "_" + create(direction === "descending", timestamp)
export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
return prefix + "_" + createIdentifier(direction === "descending", timestamp)
}
export { createID as create }
/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
export function timestamp(id: string): number {
const prefix = id.split("_")[0]
+11 -13
View File
@@ -43,24 +43,22 @@ const layer = Layer.effect(
})
const observe = Effect.fn("InstructionContext.observe")(function* () {
const start = yield* fs.resolve(location.directory)
const stop = yield* fs.resolve(location.project.directory)
const start = FSUtil.resolve(location.directory)
const stop = FSUtil.resolve(location.project.directory)
const fromProject = relative(stop, start)
const insideProject =
fromProject === "" || (fromProject !== ".." && !fromProject.startsWith(`..${sep}`) && !isAbsolute(fromProject))
const discovered = new Set(
yield* Effect.forEach(
Flag.OPENCODE_DISABLE_PROJECT_CONFIG || !insideProject
? []
: yield* fs.up({
targets: ["AGENTS.md"],
start,
stop,
}),
fs.resolve,
),
(Flag.OPENCODE_DISABLE_PROJECT_CONFIG || !insideProject
? []
: yield* fs.up({
targets: ["AGENTS.md"],
start,
stop,
})
).map(FSUtil.resolve),
)
const paths = Array.dedupe([yield* fs.resolve(join(global.config, "AGENTS.md")), ...discovered])
const paths = Array.dedupe([FSUtil.resolve(join(global.config, "AGENTS.md")), ...discovered])
const files = yield* Effect.forEach(
paths,
(path) =>
+5 -6
View File
@@ -406,7 +406,7 @@ const layer = Layer.effect(
.get()
.integrations.get(input.integrationID)
?.methods.some((method) => method.type === "key")
if (!method) return yield* Effect.die(new Error(`Key method not found: ${input.integrationID}`))
if (!method) return yield* Effect.die(`Key method not found: ${input.integrationID}`)
yield* credentials.create({
integrationID: input.integrationID,
label: input.label,
@@ -418,7 +418,7 @@ const layer = Layer.effect(
oauth: Effect.fn("Integration.connection.oauth")(function* (input) {
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
if (!method) {
return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`))
return yield* Effect.die(`OAuth method not found: ${input.integrationID}/${input.methodID}`)
}
const attemptScope = yield* Scope.fork(scope)
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
@@ -475,7 +475,7 @@ const layer = Layer.effect(
attempt: {
status: Effect.fn("Integration.attempt.status")(function* (attemptID) {
const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID)
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${attemptID}`))
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`)
if (attempt.status === "failed") {
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
}
@@ -488,13 +488,12 @@ const layer = Layer.effect(
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
})
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`)
if (attempt.status !== "pending") return
if (attempt.authorization.mode === "code" && input.code === undefined) {
return yield* new CodeRequiredError({ attemptID: input.attemptID })
}
if (attempt.completing)
return yield* Effect.die(new Error(`OAuth attempt already completing: ${input.attemptID}`))
if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`)
const callback =
attempt.authorization.mode === "auto"
? attempt.authorization.callback
+9 -7
View File
@@ -11,7 +11,7 @@ import { FileSystem } from "./filesystem"
import { FileSystemSearch } from "./filesystem/search"
import { Generate } from "./generate"
import { Form } from "./form"
import { LocationWatcher } from "./filesystem/location-watcher"
import { Watcher } from "./filesystem/watcher"
import { Image } from "./image"
import { Integration } from "./integration"
import { Location } from "./location"
@@ -21,13 +21,15 @@ import { MCP } from "./mcp/index"
import { PermissionV2 } from "./permission"
import { PluginV2 } from "./plugin"
import { PluginInternal } from "./plugin/internal"
import { Policy } from "./policy"
import { Project } from "./project"
import { ProjectCopy } from "./project/copy"
import { Pty } from "./pty"
import { QuestionV2 } from "./question"
import { Shell } from "./shell"
import { Reference } from "./reference"
import { ReferenceGuidance } from "./reference/guidance"
import { SessionRunnerLLM } from "./session/runner/llm"
import * as SessionRunnerLLM from "./session/runner/llm"
import { SessionRunnerModel } from "./session/runner/model"
import { SessionCompaction } from "./session/compaction"
import { SessionTitle } from "./session/title"
@@ -47,8 +49,10 @@ import { Vcs } from "./vcs"
export { LocationServiceMap } from "./location-service-map"
const locationServiceNodes = [
export const locationServices = LayerNode.group([
Project.node,
Location.node,
Policy.node,
Config.node,
AgentV2.node,
CommandV2.node,
@@ -62,7 +66,7 @@ const locationServiceNodes = [
ProjectCopy.refreshNode,
FileSystemSearch.node,
FileSystem.node,
LocationWatcher.node,
Watcher.node,
Pty.node,
Shell.node,
SkillV2.node,
@@ -92,9 +96,7 @@ const locationServiceNodes = [
Snapshot.node,
SessionRunnerLLM.node,
Vcs.node,
] as const satisfies readonly Node.LocationNode<unknown, unknown>[]
export const locationServices = LayerNode.group<typeof locationServiceNodes>(locationServiceNodes)
])
export type LocationServices = LayerNode.Output<typeof locationServices>
export type LocationError = LayerNode.Error<typeof locationServices>
+4 -7
View File
@@ -32,6 +32,7 @@ import { InstallationVersion } from "../installation/version"
const DEFAULT_STARTUP_TIMEOUT = 30_000
const DEFAULT_REQUEST_TIMEOUT = 30_000
const DEFAULT_TOOL_CALL_TIMEOUT = 100_000_000
type Transport = StdioClientTransport | StreamableHTTPClientTransport
@@ -46,11 +47,7 @@ const TolerantListPromptsResult = ListPromptsResultSchema.extend({
export class NeedsAuthError extends Schema.TaggedErrorClass<NeedsAuthError>()("MCP.NeedsAuthError", {
server: Schema.String,
}) {
override get message() {
return `MCP server requires authentication: ${this.server}`
}
}
}) {}
export class ConnectError extends Schema.TaggedErrorClass<ConnectError>()("MCP.ConnectError", {
server: Schema.String,
@@ -285,8 +282,8 @@ export const connect = Effect.fnUntraced(function* (
client.callTool(
{ name: input.name, arguments: input.args ?? {} },
CallToolResultSchema,
// Keep progress tokens available without imposing a client timeout on tool execution.
{ signal, resetTimeoutOnProgress: true, onprogress: () => {} },
// Human-driven tools can exceed the SDK default; use an effectively infinite timeout.
{ signal, timeout: DEFAULT_TOOL_CALL_TIMEOUT, resetTimeoutOnProgress: true, onprogress: () => {} },
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
+2 -6
View File
@@ -127,11 +127,7 @@ export class ResourceContent extends Schema.Class<ResourceContent>("MCP.Resource
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
server: ServerName,
}) {
override get message() {
return `MCP server not found: ${this.server}`
}
}
}) {}
export class ToolCallError extends Schema.TaggedErrorClass<ToolCallError>()("MCP.ToolCallError", {
server: ServerName,
@@ -205,7 +201,7 @@ export const layer = Layer.effect(
for (const [name, server] of Object.entries(entry.info.mcp?.servers ?? {})) {
runtime.set(ServerName.make(name), {
config: { ...server, timeout: { ...timeout, ...server.timeout } },
status: { status: "pending" },
status: { status: "disconnected" },
startup: Deferred.makeUnsafe<void>(),
})
}
+4 -15
View File
@@ -47,7 +47,7 @@ const Cost = Schema.Struct({
const ReasoningOption = Schema.Union([
Schema.Struct({
type: Schema.Literal("effort"),
values: Schema.Array(Schema.Union([Schema.String, Schema.Null])),
values: Schema.Array(Schema.String),
}),
Schema.Struct({
type: Schema.Literal("toggle"),
@@ -67,7 +67,7 @@ export const Model = Schema.Struct({
attachment: Schema.Boolean,
reasoning: Schema.Boolean,
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
temperature: Schema.optional(Schema.Boolean),
temperature: Schema.Boolean,
tool_call: Schema.Boolean,
interleaved: Schema.optional(
Schema.Union([
@@ -125,10 +125,6 @@ export const Provider = Schema.Struct({
export type Provider = Schema.Schema.Type<typeof Provider>
const Providers = Schema.Record(Schema.String, Provider)
const decodeProviders = Schema.decodeUnknownEffect(Schema.fromJsonString(Providers))
const decodeProvidersUnknown = Schema.decodeUnknownEffect(Providers)
export const Event = ModelsDev.Event
declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined
@@ -180,7 +176,6 @@ const layer = Layer.effect(
})
const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe(
Effect.flatMap(decodeProvidersUnknown),
Effect.catch((error) => {
if (
Flag.OPENCODE_MODELS_PATH === undefined &&
@@ -191,17 +186,11 @@ const layer = Layer.effect(
}
return Effect.succeed(undefined)
}),
Effect.map((v) => v as Record<string, Provider> | undefined),
)
const loadSnapshot = Effect.sync(() =>
typeof OPENCODE_MODELS_DEV === "undefined" ? undefined : OPENCODE_MODELS_DEV,
).pipe(
Effect.flatMap((snapshot) =>
snapshot === undefined ? Effect.succeed(undefined) : decodeProvidersUnknown(snapshot),
),
Effect.catch((cause) =>
Effect.logWarning("bundled models snapshot failed schema decode", { cause }).pipe(Effect.as(undefined)),
),
)
const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () {
@@ -232,7 +221,7 @@ const layer = Layer.effect(
return yield* fetchAndWrite()
}),
)
return yield* decodeProviders(text)
return JSON.parse(text) as Record<string, Provider>
}).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie)
const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity)
+42 -73
View File
@@ -1,7 +1,7 @@
export * as PermissionV2 from "./permission"
import { makeLocationNode } from "./effect/app-node"
import { Context, Deferred, Effect, Layer, Schema } from "effect"
import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect"
import { Permission } from "@opencode-ai/schema/permission"
import { EventV2 } from "./event"
import { Location } from "./location"
@@ -11,9 +11,7 @@ import { SessionStore } from "./session/store"
import { Wildcard } from "./util/wildcard"
import { PermissionSaved } from "./permission/saved"
const PermissionEffect = Permission.Effect
export { PermissionEffect as Effect }
export { Rule, Ruleset } from "@opencode-ai/schema/permission"
export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission"
const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
export const ID = Permission.ID
@@ -59,14 +57,7 @@ export type AskResult = typeof AskResult.Type
export const Event = Permission.Event
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {
permission: Schema.String,
resources: Schema.Array(Schema.String),
}) {
override get message() {
return `Permission rejected: ${this.permission}`
}
}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {}
export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("PermissionV2.CorrectedError", {
feedback: Schema.String,
@@ -74,13 +65,7 @@ export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("P
export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionV2.DeniedError", {
rules: Permission.Ruleset,
permission: Schema.String,
resources: Schema.Array(Schema.String),
}) {
override get message() {
return `Permission denied: ${this.permission}`
}
}
}) {}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", {
requestID: ID,
@@ -105,12 +90,12 @@ export function merge(...rulesets: Permission.Ruleset[]): Permission.Ruleset {
}
export interface Interface {
readonly ask: (input: AssertInput) => Effect.Effect<AskResult, SessionV2.NotFoundError>
readonly assert: (input: AssertInput) => Effect.Effect<void, Error | SessionV2.NotFoundError>
readonly reply: (input: ReplyInput) => Effect.Effect<void, NotFoundError>
readonly get: (id: ID) => Effect.Effect<Request | undefined>
readonly forSession: (sessionID: SessionV2.ID) => Effect.Effect<ReadonlyArray<Request>>
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
readonly ask: (input: AssertInput) => EffectRuntime.Effect<AskResult, SessionV2.NotFoundError>
readonly assert: (input: AssertInput) => EffectRuntime.Effect<void, Error | SessionV2.NotFoundError>
readonly reply: (input: ReplyInput) => EffectRuntime.Effect<void, NotFoundError>
readonly get: (id: ID) => EffectRuntime.Effect<Request | undefined>
readonly forSession: (sessionID: SessionV2.ID) => EffectRuntime.Effect<ReadonlyArray<Request>>
readonly list: () => EffectRuntime.Effect<ReadonlyArray<Request>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Permission") {}
@@ -123,7 +108,7 @@ interface Pending {
const layer = Layer.effect(
Service,
Effect.gen(function* () {
EffectRuntime.gen(function* () {
const events = yield* EventV2.Service
const location = yield* Location.Service
const agents = yield* AgentV2.Service
@@ -131,33 +116,28 @@ const layer = Layer.effect(
const saved = yield* PermissionSaved.Service
const pending = new Map<ID, Pending>()
yield* Effect.addFinalizer(() =>
Effect.forEach(
pending.values(),
(item) =>
Deferred.fail(
item.deferred,
new RejectedError({ permission: item.request.action, resources: [...item.request.resources] }),
),
{
discard: true,
},
).pipe(
Effect.ensuring(
Effect.sync(() => {
yield* EffectRuntime.addFinalizer(() =>
EffectRuntime.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), {
discard: true,
}).pipe(
EffectRuntime.ensuring(
EffectRuntime.sync(() => {
pending.clear()
}),
),
),
)
const savedRules = Effect.fnUntraced(function* () {
const savedRules = EffectRuntime.fnUntraced(function* () {
return (yield* saved.list({ projectID: location.project.id })).map(
(item): Permission.Rule => ({ action: item.action, resource: item.resource, effect: "allow" }),
)
})
const configured = Effect.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID, agentID?: AgentV2.ID) {
const configured = EffectRuntime.fn("PermissionV2.configured")(function* (
sessionID: SessionV2.ID,
agentID?: AgentV2.ID,
) {
const session = yield* sessions.get(sessionID)
if (!session) return yield* new SessionV2.NotFoundError({ sessionID })
const agent = yield* agents.resolve(agentID ?? session.agent)
@@ -172,7 +152,7 @@ const layer = Layer.effect(
return rules.filter((rule) => Wildcard.match(input.action, rule.action))
}
const evaluateInput = Effect.fnUntraced(function* (input: AssertInput) {
const evaluateInput = EffectRuntime.fnUntraced(function* (input: AssertInput) {
const rules = yield* configured(input.sessionID, input.agent)
if (denied(input, rules)) return { effect: "deny" as const, rules }
const all = [...rules, ...(yield* savedRules())]
@@ -194,43 +174,40 @@ const layer = Layer.effect(
}
const create = (request: Request, agent?: AgentV2.ID) =>
Effect.uninterruptible(
Effect.gen(function* () {
EffectRuntime.uninterruptible(
EffectRuntime.gen(function* () {
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
const item = { request, agent, deferred }
if (pending.has(request.id))
return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`))
if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`)
pending.set(request.id, item)
yield* events
.publish(Event.Asked, request)
.pipe(Effect.onError(() => Effect.sync(() => pending.delete(request.id))))
.pipe(EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id))))
return item
}),
)
const ask = Effect.fn("PermissionV2.ask")(function* (input: AssertInput) {
const ask = EffectRuntime.fn("PermissionV2.ask")(function* (input: AssertInput) {
const result = yield* evaluateInput(input)
const value = request(input)
if (result.effect === "ask") yield* create(value, input.agent)
return { id: value.id, effect: result.effect }
})
const assert = Effect.fn("PermissionV2.assert")((input: AssertInput) =>
Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const assert = EffectRuntime.fn("PermissionV2.assert")((input: AssertInput) =>
EffectRuntime.uninterruptibleMask((restore) =>
EffectRuntime.gen(function* () {
const result = yield* evaluateInput(input)
if (result.effect === "deny") {
return yield* new DeniedError({
rules: relevant(input, result.rules),
permission: input.action,
resources: input.resources,
})
}
if (result.effect === "allow") return
const item = yield* create(request(input), input.agent)
return yield* restore(Deferred.await(item.deferred)).pipe(
Effect.ensuring(
Effect.sync(() => {
EffectRuntime.ensuring(
EffectRuntime.sync(() => {
pending.delete(item.request.id)
}),
),
@@ -239,9 +216,9 @@ const layer = Layer.effect(
),
)
const reply = Effect.fn("PermissionV2.reply")((input: ReplyInput) =>
Effect.uninterruptible(
Effect.gen(function* () {
const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) =>
EffectRuntime.uninterruptible(
EffectRuntime.gen(function* () {
const existing = pending.get(input.requestID)
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
yield* events.publish(Event.Replied, {
@@ -253,12 +230,7 @@ const layer = Layer.effect(
if (input.reply === "reject") {
yield* Deferred.fail(
existing.deferred,
input.message
? new CorrectedError({ feedback: input.message })
: new RejectedError({
permission: existing.request.action,
resources: [...existing.request.resources],
}),
input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(),
)
pending.delete(input.requestID)
for (const [id, item] of pending) {
@@ -268,10 +240,7 @@ const layer = Layer.effect(
requestID: item.request.id,
reply: "reject",
})
yield* Deferred.fail(
item.deferred,
new RejectedError({ permission: item.request.action, resources: [...item.request.resources] }),
)
yield* Deferred.fail(item.deferred, new RejectedError())
pending.delete(id)
}
return
@@ -292,7 +261,7 @@ const layer = Layer.effect(
for (const [id, item] of pending) {
const input = { ...item.request }
const rules = yield* configured(item.request.sessionID, item.agent).pipe(
Effect.catchTag("Session.NotFoundError", () => Effect.succeed(undefined)),
EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)),
)
if (!rules) continue
if (denied(input, rules)) continue
@@ -315,15 +284,15 @@ const layer = Layer.effect(
),
)
const list = Effect.fn("PermissionV2.list")(function* () {
const list = EffectRuntime.fn("PermissionV2.list")(function* () {
return Array.from(pending.values(), (item) => item.request)
})
const get = Effect.fn("PermissionV2.get")(function* (id: ID) {
const get = EffectRuntime.fn("PermissionV2.get")(function* (id: ID) {
return pending.get(id)?.request
})
const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) {
const forSession = EffectRuntime.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) {
return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID)
})
+2 -2
View File
@@ -48,7 +48,7 @@ const layer = Layer.effect(
let host: Parameters<PluginDefinition["effect"]>[0]
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginDefinition["effect"]) {
if (loading.has(id)) return yield* Effect.die(new Error(`Plugin load cycle detected for ${id}`))
if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
yield* locks.withLock(id)(
Effect.sync(() => {
@@ -90,7 +90,7 @@ const layer = Layer.effect(
})
const remove = Effect.fn("Plugin.remove")(function* (id: ID) {
if (loading.has(id)) return yield* Effect.die(new Error(`Cannot remove plugin ${id} while it is loading`))
if (loading.has(id)) return yield* Effect.die(`Cannot remove plugin ${id} while it is loading`)
yield* locks.withLock(id)(
State.batch(
+23 -77
View File
@@ -1,14 +1,12 @@
export * as PluginHost from "./host"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { Effect, Schema, Stream } from "effect"
import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect"
import { Effect, Schema } from "effect"
import { AgentV2 } from "../agent"
import { AISDK } from "../aisdk"
import { Catalog } from "../catalog"
import { CommandV2 } from "../command"
import { Credential } from "../credential"
import { EventV2 } from "../event"
import { Integration } from "../integration"
import { Location } from "../location"
import { ModelV2 } from "../model"
@@ -23,14 +21,12 @@ import { ToolHooks } from "../tool/hooks"
import { WorkspaceV2 } from "../workspace"
const mutable = <T>(value: T) => value as DeepMutable<T>
const isEvent = Schema.is(Schema.Union(EventManifest.ServerDefinitions))
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
const agents = yield* AgentV2.Service
const aisdk = yield* AISDK.Service
const catalog = yield* Catalog.Service
const commands = yield* CommandV2.Service
const events = yield* EventV2.Service
const integration = yield* Integration.Service
const location = yield* Location.Service
const reference = yield* Reference.Service
@@ -44,7 +40,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
workspaceID: location.workspaceID,
project: location.project,
})
const locationRef = (input?: Parameters<PluginContext["agent"]["list"]>[0]) =>
const locationRef = (input?: Parameters<Interface["agent"]["list"]>[0]) =>
input?.location === undefined
? undefined
: Location.Ref.make({
@@ -56,8 +52,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
})
const isCurrentLocation = (ref: Location.Ref) =>
ref.directory === location.directory && ref.workspaceID === location.workspaceID
const response = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(Effect.map((data) => ({ location: locationInfo(), data })))
return {
options: {},
@@ -69,15 +63,15 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
},
reload: agents.reload,
transform: (callback) =>
agents.transform((draft) => {
agents.transform((draft) =>
callback({
list: () => mutable(draft.list()),
get: (id) => mutable(draft.get(AgentV2.ID.make(id))),
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
update: (id, update) => draft.update(AgentV2.ID.make(id), update),
remove: (id) => draft.remove(AgentV2.ID.make(id)),
})
}),
}),
),
},
aisdk: {
sdk: (callback) =>
@@ -108,26 +102,9 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}),
},
catalog: {
provider: {
list: () => response(catalog.provider.available()),
get: (input) =>
catalog.provider
.get(ProviderV2.ID.make(input.providerID))
.pipe(
Effect.flatMap((provider) =>
provider === undefined
? Effect.fail(new Error(`Provider not found: ${input.providerID}`))
: response(Effect.succeed(provider)),
),
),
},
model: {
list: () => response(catalog.model.available()),
default: () => response(catalog.model.default()),
},
reload: catalog.reload,
transform: (callback) =>
catalog.transform((draft) => {
catalog.transform((draft) =>
callback({
provider: {
list: () => mutable(draft.provider.list()),
@@ -148,42 +125,14 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
},
},
})
}),
},
command: {
list: () => response(commands.list()),
reload: commands.reload,
transform: (callback) =>
commands.transform((draft) => {
callback(draft)
}),
},
event: {
subscribe: () => events.live().pipe(Stream.filter(isEvent)),
},
integration: {
list: () => response(integration.list()),
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
connectKey: (input) =>
integration.connection.key({
integrationID: Integration.ID.make(input.integrationID),
key: input.key,
label: input.label,
}),
connectOauth: (input) =>
response(
integration.connection.oauth({
integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID),
inputs: input.inputs,
label: input.label,
}),
),
attemptStatus: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
attemptComplete: (input) =>
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
attemptCancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
},
command: {
reload: commands.reload,
transform: commands.transform,
},
integration: {
reload: integration.reload,
connection: {
active: (id) => integration.connection.active(Integration.ID.make(id)),
@@ -193,7 +142,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
),
},
transform: (callback) =>
integration.transform((draft) => {
integration.transform((draft) =>
callback({
list: () => mutable(draft.list()),
get: (id) => mutable(draft.get(Integration.ID.make(id))),
@@ -270,36 +219,33 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
remove: (id, method) =>
draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)),
},
})
}),
}),
),
},
plugin: {
list: () => response(plugin.list()),
add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect),
remove: (id) => plugin.remove(PluginV2.ID.make(id)),
},
reference: {
list: () => response(reference.list()),
reload: reference.reload,
transform: (callback) =>
reference.transform((draft) => {
reference.transform((draft) =>
callback({
add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)),
remove: draft.remove,
list: draft.list,
})
}),
}),
),
},
skill: {
list: () => response(skill.list()),
reload: skill.reload,
transform: (callback) =>
skill.transform((draft) => {
skill.transform((draft) =>
callback({
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
list: draft.list,
})
}),
}),
),
},
tool: {
register: (input) => tools.register(input),
@@ -359,5 +305,5 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
command: runtime.session.command,
interrupt: (input) => runtime.session.interrupt(input.sessionID),
},
} satisfies PluginContext
} satisfies Interface
})
+54 -55
View File
@@ -201,65 +201,64 @@ export const ModelsDevPlugin = define({
effect: Effect.fn(function* (ctx) {
const modelsDev = yield* ModelsDev.Service
const events = yield* EventV2.Service
const loaded = { data: yield* modelsDev.get() }
yield* ctx.integration.transform((integrations) => {
for (const item of Object.values(loaded.data)) {
if (item.env.length === 0) continue
const integrationID = item.id
integrations.update(integrationID, (integration) => (integration.name = item.name))
integrations.method.update({
integrationID,
method: { type: "key" },
})
integrations.method.update({
integrationID,
method: { type: "env", names: [...item.env] },
})
}
})
yield* ctx.catalog.transform((catalog) => {
for (const item of Object.values(loaded.data)) {
const providerID = ProviderV2.ID.make(item.id)
catalog.provider.update(providerID, (provider) => {
provider.name = item.name
provider.api = item.npm
? {
type: "aisdk",
package: item.npm,
url: item.api,
}
: {
type: "native",
url: item.api,
settings: {},
}
})
yield* ctx.integration.transform(
Effect.fn(function* (integrations) {
const data = yield* modelsDev.get()
for (const item of Object.values(data)) {
if (item.env.length === 0) continue
const integrationID = item.id
integrations.update(integrationID, (integration) => (integration.name = item.name))
integrations.method.update({
integrationID,
method: { type: "key" },
})
integrations.method.update({
integrationID,
method: { type: "env", names: [...item.env] },
})
}
}),
)
yield* ctx.catalog.transform(
Effect.fn(function* (catalog) {
const data = yield* modelsDev.get()
for (const item of Object.values(data)) {
const providerID = ProviderV2.ID.make(item.id)
catalog.provider.update(providerID, (provider) => {
provider.name = item.name
provider.api = item.npm
? {
type: "aisdk",
package: item.npm,
url: item.api,
}
: {
type: "native",
url: item.api,
settings: {},
}
})
for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost)
const variants = reasoningVariants(item, model)
catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants }))
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>
applyModel(draft, model, {
name: modeName(model, mode),
cost: mergeCost(baseCost, options.cost),
request: options.provider,
variants,
}),
)
for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost)
const variants = reasoningVariants(item, model)
catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants }))
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>
applyModel(draft, model, {
name: modeName(model, mode),
cost: mergeCost(baseCost, options.cost),
request: options.provider,
variants,
}),
)
}
}
}
}
})
}),
)
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
Stream.runForEach(() =>
modelsDev.get().pipe(
Effect.tap((data) => Effect.sync(() => (loaded.data = data))),
Effect.andThen(ctx.integration.reload()),
Effect.andThen(ctx.catalog.reload()),
),
),
Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))),
Effect.forkScoped({ startImmediately: true }),
)
}),
+10 -41
View File
@@ -1,11 +1,12 @@
export * as PluginPromise from "./promise"
import { define } from "@opencode-ai/plugin/v2/effect"
import type { Plugin, PluginContext } from "@opencode-ai/plugin/v2/promise"
import { Effect, Scope, Stream } from "effect"
import type { Plugin, PluginContext, Registration } from "@opencode-ai/plugin/v2/promise"
import { Effect, Scope } from "effect"
// The Effect host hands back this registration shape; mirror it structurally so
// we do not have to alias the Effect package's `Registration` against the Promise one.
type HostRegistration = { readonly dispose: Effect.Effect<void> }
type Registration = { readonly dispose: () => Promise<void> }
/**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
@@ -30,23 +31,20 @@ export function fromPromise(plugin: Plugin) {
dispose: () => Effect.runPromiseWith(context)(registration.dispose),
}))
const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(context)(effect)
const run = (effect: Effect.Effect<void>) => Effect.runPromiseWith(context)(effect)
const transform =
<Draft>(domain: {
transform: (callback: (draft: Draft) => void) => Effect.Effect<HostRegistration, never, Scope.Scope>
transform: (
callback: (draft: Draft) => Effect.Effect<void> | void,
) => Effect.Effect<HostRegistration, never, Scope.Scope>
}) =>
(callback: (draft: Draft) => void) =>
register(
domain.transform((draft) => {
callback(draft)
}),
)
(callback: (draft: Draft) => Promise<void> | void) =>
register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft)))))
const context2: PluginContext = {
options: host.options,
agent: {
list: (input) => run(host.agent.list(input)),
transform: transform(host.agent),
reload: () => run(host.agent.reload()),
},
@@ -57,33 +55,14 @@ export function fromPromise(plugin: Plugin) {
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
},
catalog: {
provider: {
list: (input) => run(host.catalog.provider.list(input)),
get: (input) => run(host.catalog.provider.get(input)),
},
model: {
list: (input) => run(host.catalog.model.list(input)),
default: (input) => run(host.catalog.model.default(input)),
},
transform: transform(host.catalog),
reload: () => run(host.catalog.reload()),
},
command: {
list: (input) => run(host.command.list(input)),
transform: transform(host.command),
reload: () => run(host.command.reload()),
},
event: {
subscribe: () => Stream.toAsyncIterable(host.event.subscribe()),
},
integration: {
list: (input) => run(host.integration.list(input)),
get: (input) => run(host.integration.get(input)),
connectKey: (input) => run(host.integration.connectKey(input)),
connectOauth: (input) => run(host.integration.connectOauth(input)),
attemptStatus: (input) => run(host.integration.attemptStatus(input)),
attemptComplete: (input) => run(host.integration.attemptComplete(input)),
attemptCancel: (input) => run(host.integration.attemptCancel(input)),
transform: transform(host.integration),
reload: () => run(host.integration.reload()),
connection: {
@@ -92,7 +71,6 @@ export function fromPromise(plugin: Plugin) {
},
},
plugin: {
list: (input) => run(host.plugin.list(input)),
add: (input) => {
const child = fromPromise(input)
return run(host.plugin.add(child))
@@ -100,22 +78,13 @@ export function fromPromise(plugin: Plugin) {
remove: (id) => run(host.plugin.remove(id)),
},
reference: {
list: (input) => run(host.reference.list(input)),
transform: transform(host.reference),
reload: () => run(host.reference.reload()),
},
skill: {
list: (input) => run(host.skill.list(input)),
transform: transform(host.skill),
reload: () => run(host.skill.reload()),
},
session: {
create: (input) => run(host.session.create(input)),
get: (input) => run(host.session.get(input)),
prompt: (input) => run(host.session.prompt(input)),
command: (input) => run(host.session.command(input)),
interrupt: (input) => run(host.session.interrupt(input)),
},
}
yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
@@ -62,20 +62,22 @@ function selectMantleModel(sdk: MantleSDK, modelID: string) {
export const AmazonBedrockPlugin = define({
id: "amazon-bedrock",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return
if (typeof provider.request.body.endpoint !== "string") return
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
provider.api.url = provider.request.body.endpoint
delete provider.request.body.endpoint
})
}
})
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return
if (typeof provider.request.body.endpoint !== "string") return
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
provider.api.url = provider.request.body.endpoint
delete provider.request.body.endpoint
})
}
}),
)
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
+12 -10
View File
@@ -4,16 +4,18 @@ import { define } from "../internal"
export const AnthropicPlugin = define({
id: "anthropic",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["anthropic-beta"] =
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
})
}
})
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["anthropic-beta"] =
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
})
}
}),
)
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/anthropic") return
+29 -25
View File
@@ -13,19 +13,21 @@ function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
export const AzurePlugin = define({
id: "azure",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/azure") continue
const configured = item.provider.request.body.resourceName
const resourceName =
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
if (!resourceName) continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.body.resourceName = resourceName
})
}
})
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/azure") continue
const configured = item.provider.request.body.resourceName
const resourceName =
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
if (!resourceName) continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.body.resourceName = resourceName
})
}
}),
)
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/azure") return
@@ -56,18 +58,20 @@ export const AzurePlugin = define({
export const AzureCognitiveServicesPlugin = define({
id: "azure-cognitive-services",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (!resourceName) return
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (!item.provider.id.includes("azure-cognitive-services")) continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
})
}
})
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (!resourceName) return
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (!item.provider.id.includes("azure-cognitive-services")) continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
})
}
}),
)
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
+11 -9
View File
@@ -4,15 +4,17 @@ import { define } from "../internal"
export const CerebrasPlugin = define({
id: "cerebras",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/cerebras") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
})
}
})
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/cerebras") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
})
}
}),
)
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cerebras") return
@@ -9,16 +9,18 @@ const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
export const CloudflareWorkersAIPlugin = define({
id: "cloudflare-workers-ai",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
const item = evt.provider.get(providerID)
if (!item) return
evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return
if (provider.api.url) return
const accountId = resolveAccountId(provider.request.body)
if (accountId) provider.api.url = workersEndpoint(accountId)
})
})
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
const item = evt.provider.get(providerID)
if (!item) return
evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return
if (provider.api.url) return
const accountId = resolveAccountId(provider.request.body)
if (accountId) provider.api.url = workersEndpoint(accountId)
})
}),
)
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return

Some files were not shown because too many files have changed in this diff Show More