diff --git a/.editorconfig b/.editorconfig index 734a03600..6cf65c4e7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -699,7 +699,9 @@ dotnet_diagnostic.S6602.severity = none # ============================================================================= # SECTION 13: COMPILER DIAGNOSTICS (CSxxxx) # ============================================================================= -dotnet_diagnostic.CS1591.severity = none +# Every public member on the shipped surface is documented: generated docs come from the +# spec, hand-written docs by hand (ADR-0004). Tests are exempted in Section 15. +dotnet_diagnostic.CS1591.severity = error # ============================================================================= # SECTION 14: GENERATED CODE @@ -717,6 +719,7 @@ generated_code = true # ============================================================================= [{*Tests/**/*.cs,tests/**/*.cs}] dotnet_diagnostic.CA1707.severity = none +dotnet_diagnostic.CS1591.severity = none # TUnit Assert.That reports false positives for MA0042 because the analyzer cannot model the assertion API shape. dotnet_diagnostic.MA0042.severity = none @@ -781,3 +784,17 @@ dotnet_diagnostic.S2325.severity = none # enabled for every other file. [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/LiteralKind.cs] dotnet_diagnostic.CA1720.severity = none + +# Generated response adapters gain shared, non-virtual mapping helpers on this +# base at emission build-out, and default interface members cannot carry shared +# behavior on the locked net472/netstandard2.0 targets (CS8701). Winner: the +# abstract adapter base; S1694 remains enabled for every other file. +[src/OpenCode.Sdk/Internal/ResponseAdapter.cs] +dotnet_diagnostic.S1694.severity = none + +# The generated route surface deliberately nests one static container per operation group +# (maintainer-approved shape, 2026-08-13); flattening would erase the grouping the public +# API promises. Winner: the generated route shape; CA1034 remains enabled for every other +# file. +[src/OpenCode.Sdk/OpenCodeRoutes.cs] +dotnet_diagnostic.CA1034.severity = none diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aec648eb3..c8b5d2ae5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,11 @@ jobs: if: runner.os == 'Linux' run: ./tools/opencode-tool.cs --help + # Regenerating the pinned profile must be a byte-identical no-op on a clean checkout. + - name: Verify generated output + if: runner.os == 'Linux' + run: dotnet run --file tools/opencode-tool.cs -- generate --verify + # TUnit's always-on HTML report self-uploads as a workflow artifact, but only if these # runtime vars (normally JS/Docker-action-only) are re-exported to run steps. - name: Enable TUnit report artifact upload diff --git a/AGENTS.md b/AGENTS.md index 9cb664dff..973071577 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,12 +21,11 @@ lazily); this list is the complete inventory of what is settled. Do not reopen w evidence. The design specs under `docs/superpowers/` are vision/reference material — direction and rationale, not law; only this list and the ADRs bind. -- **Target artifact:** upstream's `packages/sdk/openapi.json` (OpenAPI 3.1), pinned under - `spec/` — the same spec the official JS SDK generates from. -- **API surface:** both generations of the pinned 1.x spec — the modern block takes the - unmarked public names ("V2" never appears); legacy lives behind a legacy-marked - sub-surface, deleted at our upstream-absorbing major; legacy deep-testing is - consumer-driven (ADR-0005). +- **Target artifact:** upstream's `packages/protocol/openapi.json` from the active `v2` + branch (OpenAPI 3.1), pinned as a snapshot under `spec/` (ADR-0005; the 1.x pin retires + at the M1 retarget task). +- **API surface:** the v2 protocol surface only — public names strip the `v2.` operationId + prefix ("V2" never appears) (ADR-0005). - **Hybrid construction:** hand-written behavior core; models *and* operation methods from our own Roslyn-emission generator; spec ingestion rides the pinned `Microsoft.OpenApi` reader — the generator owns a fail-closed semantic projection, never an OpenAPI parser; @@ -45,8 +44,9 @@ direction and rationale, not law; only this list and the ADRs bind. - **Monorepo, independent versioning:** per-merge GitHub Packages CD, manual NuGet.org releases (ADR-0006). - **Licensing:** MIT (`PackageLicenseFile`). -- **SSE:** `IAsyncEnumerable`, no auto-reconnect; the durable stream resumes via the - `after` cursor (research doc 02). +- **SSE:** `IAsyncEnumerable`, no auto-reconnect (research doc 02); the durable-stream + resume design is re-derived against the v2 surface at M3 (`v2.session.log` carries + `after`/`follow`). - **`ConfigureAwait(false)`:** triple-enforced in product code, off in tests (research doc 07). - **Analyzer policy:** fail-closed maximalist (research doc 07; relitigation ban: Hard @@ -116,6 +116,14 @@ direction and rationale, not law; only this list and the ADRs bind. - **Defensive programming is the default, everywhere:** guard public inputs, assert internal invariants, fail loudly rather than guess — silent fallbacks exist only as explicitly recorded tolerances (ADR-0009 pattern). +- **Performance is a standing concern, weighted by artifact:** the shipped SDK targets both + speed and minimal allocation on its hot paths (zero *avoidable* allocations — ADR-0004's + immutable models and ADR-0007's raw-body retention are contracts, not waste); repo tooling + targets speed. No speculative optimization, but obvious waste (redundant parses, needless + copies, avoidable buffering) is always evaluated, starting with the low-hanging fruit. + Performance claims are settled by benchmarks (`tests/OpenCode.Sdk.Performance.Tests`, + `MemoryDiagnoser` on), never by argument; performance-touching PRs carry before/after + numbers. - **Code style is canonical in `docs/engineering/coding-style.md`:** named collaborator classes over private-method accumulation; interfaces at seams with full-battery DI-first executable composition, sealed everywhere else; no tuple returns or concrete-collection diff --git a/CONTEXT.md b/CONTEXT.md index 3a74725e2..5ba8e767d 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -52,20 +52,17 @@ One project-directory context inside a Server process, selected per request via targeting; a Server process hosts many. **Directory targeting**: -Per-request project targeting via the `x-opencode-directory` header. +Per-request project targeting via `location[...]` query parameters (the v2 convention). **PTY**: A pseudo-terminal session managed through the API. ### This project's language -**Modern surface**: -The `v2.*`-prefixed operation block of the pinned spec; public names carry no prefix. -_Avoid_: v2, V2 (in public naming) - -**Legacy surface**: -The un-prefixed operation block of the pinned spec; lives behind a legacy-marked sub-surface -and is deleted wholesale at the 2.0-absorbing major. +**Protocol surface** (historically "modern surface" in dated research docs): +The `v2.*`-prefixed protocol operation block — the surface this SDK generates (ADR-0005); +public names carry no prefix. +_Avoid_: v2, V2 (in public naming); legacy (the retired 1.x dual-surface vocabulary) **Launcher**: The in-core component that starts, monitors, and stops a local `opencode serve` process. diff --git a/Directory.Packages.props b/Directory.Packages.props index fc4a1e414..769b76b8a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,6 +29,7 @@ + @@ -41,6 +42,7 @@ + diff --git a/OpenCode.slnx b/OpenCode.slnx index 0770c524c..2254cb7a1 100644 --- a/OpenCode.slnx +++ b/OpenCode.slnx @@ -16,6 +16,8 @@ + + diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9f5b04031..9220ea5ef 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,18 +1,33 @@ # Roadmap -Date: 2026-08-12 +Date: 2026-08-13 Operational state: what is done, what is next, what is open. This file shrinks as work lands. Evergreen rules and locked decisions live in `../AGENTS.md`; decision records in `adr/`. ## Status -Structural skeleton, three-OS CI, the pinned spec snapshot, Slice 0 tooling, and complete-pin -SpecIR ingestion are in place. M1's compiler arc selects the two walking-skeleton operations, -binds their reachable closure, emits committed Roslyn-built models plus source-generated JSON -metadata, and owns deterministic `generate` / `generate --verify` writes through a guarded -manifest. The partial-operation marker blocks every package while breadth remains incomplete. -The next M1 arc is the minimal transport and callable client with typed errors and `NoThrow`. +**M1 is complete.** The SDK targets the v2 protocol surface only (ADR-0005; `spec/` pins +v2-branch commit `a6a712a`), and the walking skeleton runs end to end: SpecIR plus curation +bind into client/operation/envelope/route/error plans, generic emitters render the callable +surface as committed source under `src/OpenCode.Sdk` (`OpenCodeClient.GetHealthAsync`, +`Sessions.GetSessionClient(...)`, `SessionClient.GetMessageAsync`, guarded envelopes, +`OpenCodeRoutes`, response adapters), and one hand-written `Pipeline` owns endpoint authority, +Basic-auth/User-Agent decoration, buffering, throw-versus-`NoThrow`, and transport-failure +mapping. The public API is locked in a reviewed `PublicApiGenerator` baseline; the writer +refuses unmanifested overwrites and headerless manifest entries; packing still fails on the +partial-operation marker while breadth is pending. Demonstrated live 2026-08-13 against +`opencode2 serve` v0.0.0-next-17403 (pin `a6a712a` — deliberate version skew accepted): both +generated methods returned typed 200 payloads (`ServiceHealth`; `SessionMessageAssistant` +with its wire `id`). + +A verified multi-agent review of PR #16 (2026-08-13) produced a milestone-anchored queue: +the blocker set (typed-spine leaks, silent route rewriting) lands on the PR branch itself +(#17) together with the performance-test infrastructure (#18); every other finding lives in +issues #19–#25 pinned to the milestone that resolves it — nothing on that list outlives the +M series. + +The next work is M2 — the first breadth batch, planned when it starts. ## Milestones @@ -23,40 +38,52 @@ is revisited at each milestone boundary. 1. **M1 — Walking skeleton.** `v2.health.get` + `v2.session.message` end to end (SpecDocument → Binder → EmitPlan → Roslyn emitters → committed source under `src/OpenCode.Sdk` → minimal transport core → callable client), demonstrated once by - hand against a real `opencode serve` with the output pasted into the PR. Two - independently mergeable arcs: selected compiler + committed models, then the callable - client with typed errors and `NoThrow`. Design reference: + hand against a real `opencode2 serve` with the output pasted into the PR. Arc B opens + with the v2 retarget task (pin snapshot, ingestion-wall admit rule, regenerated + closure). Two independently mergeable arcs: selected compiler + committed models + (landed), then the callable client with typed errors and `NoThrow`. Design reference: `superpowers/specs/2026-08-11-production-walking-skeleton-design.md`. 2. **M2 — Breadth batches.** The generation profile grows in vertical operation batches; each batch lands its curation rows, reachable models, operation methods, and contract - tests together. -3. **M3 — Streams.** SSE engine, live/durable subscribe with `after` resume; demo: watching - a real session's event stream. The net472 `ServicePointManager` item lands here. -4. **M4 — Launcher.** `OpenCodeServer.StartAsync` with three-OS acceptance (ADR-0001); - demo: the SDK starts the server itself and calls health. The net472 stdout/tree-kill - items land here. -5. **M5 — Full surface.** Legacy hub, complete generation profile, exclusion fingerprints - (ADR-0008), packaging unblocked. + tests together. Opens with the review hardening batch (#19 `Unknown*` serialization, + #20 password-semantics decision, #25 IVT decision); the generator fail-closed walls + batch (#21, includes the P2 envelope fold) and the naming/curation wall batch (#22) + land at this boundary. +3. **M3 — Streams.** SSE engine over the v2 stream surface (`v2.event.subscribe`, + `v2.session.log` with `after`/`follow`, cursor-paged `v2.message.list`); the v1 + durable-stream design does not carry over and is re-derived here. Demo: watching a + real session's event stream. The net472 `ServicePointManager` item lands here. The + union single-pass deserialization and streaming adapter-boundary redesign (#23) land + on the M3 runway, gated on the performance baselines (#18). +4. **M4 — Launcher.** `OpenCodeServer.StartAsync` with three-OS acceptance (ADR-0001) + over `opencode2 serve`; demo: the SDK starts the server itself and calls health. The + net472 stdout/tree-kill items land here. (`serve --stdio`'s stdin leash and the + background service's discovery file are candidate mechanisms — decided in the M4 plan; + platform detail: research doc 15.) +5. **M5 — Full surface.** Complete generation profile over the protocol surface, + exclusion fingerprints (ADR-0008), packaging unblocked. 6. **M6 — Operational closure.** `refresh-spec`, Extensions DI breadth, - retry/telemetry/hooks, quarantine lane, nightly canary; durable decisions distill into - ADRs and the `superpowers/` documents retire. + retry/telemetry/hooks, quarantine lane, nightly canary (the performance suite joins + it); durable decisions distill into ADRs and the `superpowers/` documents retire. Any + hygiene-sweep leftovers (#24) are resolved here — nothing from the review queue + survives the M series. ## Open Questions +- **v2 GA watch** — the v2 line ships as `opencode2` (npm `@opencode-ai/cli@next`, desktop + beta via `update.opencode.ai`) with no GA date; the spec pin stays a deliberate snapshot, + refreshed at milestone boundaries. Platform detail: research doc 15. +- **`v2.session.log` semantics** — it replaces the v1 durable stream (`after` + `follow` on + an experimental path); resume guarantees are unestablished. Owned by M3. - **Spec refresh cadence** — the `refresh-spec` tool lands in M6; the cadence policy stays open. -- **Structural-union emission shape** — the five structural-union pin sites - (`Config.formatter` et al.) need an emission decision when a breadth batch first reaches - one (a public API review). +- **Structural-union emission shape** — the v1 pin had five structural-union sites + (`Config.formatter` et al.); the population is re-censused at the retarget, and the + emission decision lands when a breadth batch first reaches one (a public API review). - **Release mechanics** — decided parts live in ADR-0006 (independent semver, per-merge GitHub Packages CD, manual NuGet.org releases). Pre-1.0 numbering, `VersionPrefix`, RELEASE_NOTES flow, and the concrete workflows are scheduled when the first publishable increment approaches. -- **Upstream v2 watch:** the active upstream `v2` branch publishes its spec at - `packages/protocol/openapi.json`; its dialect adds single-element `allOf` wrappers - (validation keywords, occasionally with annotations), keeps `v2.`-prefixed operationIds, - and returns literals to single-value `enum` (research log sessions 12–13). No plan impact - today; re-check at each spec refresh and before the upstream-absorbing major. ## Known Gaps diff --git a/docs/adr/0004-generated-model-principles.md b/docs/adr/0004-generated-model-principles.md index 037a827b6..265cf0c64 100644 --- a/docs/adr/0004-generated-model-principles.md +++ b/docs/adr/0004-generated-model-principles.md @@ -1,24 +1,24 @@ # Generated models: immutable, required, empty-not-null -Date: 2026-08-08 +Date: 2026-08-13 Generated models are immutable by default (records, `init`-only properties, read-only collections) and mirror the spec's `required` with the C# `required` modifier. Nullability is -a last resort: absent collections deserialize to empty instead of null, and nullable -annotations appear only where absence carries meaning in the contract. This trades wire-shape -fidelity (null vs missing vs empty collapses where the distinction carries no meaning) for -consumer ergonomics, thread-safety, and AOT-friendly serialization — a deliberate deviation a -reader might otherwise "fix" back toward spec-literal emission. These principles are generator +a last resort: absent optional collections deserialize to empty instead of null, while an +explicit wire `null` is rejected unless the schema admits null; nullable annotations appear +only where null carries meaning in the contract. This trades missing-vs-empty fidelity where +the distinction carries no meaning for consumer ergonomics, thread-safety, and AOT-friendly +serialization without weakening the schema's null contract. These principles are generator policy: they apply mechanically to every emitted type. -## Generator policy additions (2026-08-09, API design + grill sessions) +## Generator policy - **`Uri` for URL-semantic properties** (spec `format: uri` or curation-marked); filesystem paths stay `string`. CA1056/CA1054 firing on a generated `*Url` string property is the fail-closed detector; a per-property fallback to `string` is the recorded escape for version-skew malformed URLs. -- **Acronym casing follows the Framework Design Guidelines** (`ApiError`, `Pty`, `Mcp`, - `Tui`; two-letter acronyms upper; brand spellings via curated exceptions — `OAuth`). +- **Acronyms use ordinary PascalCase regardless of length** (`Id`, `Io`, `Ip`, `Url`, + `Api`, `Pty`, `Mcp`, `Tui`); brand spellings use curated exceptions (`OAuth`). - **Identifier mapping is mechanical** — every wire name maps to PascalCase with `[JsonPropertyName]` carrying wire fidelity (`_tag`, snake_case, dotted schema names). diff --git a/docs/adr/0005-both-api-surfaces.md b/docs/adr/0005-both-api-surfaces.md deleted file mode 100644 index 387b94d4e..000000000 --- a/docs/adr/0005-both-api-surfaces.md +++ /dev/null @@ -1,29 +0,0 @@ -# Generate both API surfaces of the pinned 1.x spec - -Date: 2026-08-08 - -The pinned v1.18.15 spec carries two surfaces: 127 legacy operations and 61 transitional -`v2.*`-prefixed operations. We generate both — superseding the earlier v2-only decision — -because the modern block does not yet cover the product's full capability (upstream's own TUI -still runs 91 legacy vs 18 modern call sites) and the MCP-server goal needs all of it today. -Public naming strips the `v2.` prefix and never bakes "V2" into type or client names — the -modern names are upstream's newest naming layer, so there is no rename wave to absorb -(`docs/research/10-v2-to-2.0-operation-mapping.md` corrected doc 09's direction: the "2.0 -branch" spec is an April-2026 ancestor of today's root surface, not the next major). The -legacy area is deleted at a major release of ours when upstream ships its next major; that -major's shape and date are unverified — the direction rests on live signals (TUI migration -to the modern surface, in-code deprecation markers, the protocol-package extraction). -Evidence: `docs/research/09-upstream-v1v2.md`, `docs/research/10-v2-to-2.0-operation-mapping.md`. - -## Consequences - -- **Name collisions are resolved by structural separation.** With the prefix stripped, 16 of - 61 modern operation names collide with legacy ones (`session.get`, `session.prompt`, - `event.subscribe`, all `pty.*`, …). The modern surface takes the unmarked names; the legacy - surface lives behind an explicitly legacy-marked sub-surface (exact API shape decided in the - API design session). At our 2.0-absorbing major the legacy area is deleted wholesale without - touching modern names. -- **Legacy testing is consumer-driven, not uniformly best-effort.** Deep integration testing - targets the modern surface plus every legacy operation the MCP server consumes — a set - derived mechanically from the in-repo MCP project's SDK calls (ADR-0006). The remaining - legacy surface is best-effort. diff --git a/docs/adr/0005-v2-protocol-surface-only.md b/docs/adr/0005-v2-protocol-surface-only.md new file mode 100644 index 000000000..7bff375b1 --- /dev/null +++ b/docs/adr/0005-v2-protocol-surface-only.md @@ -0,0 +1,32 @@ +# Target the v2 protocol surface only + +Date: 2026-08-13 + +opencode's active successor line (branch `v2`) serves exactly one HTTP surface — the protocol +contract behind the `v2.*`-prefixed operationIds — and has deleted the legacy root block +wholesale: `packages/opencode` no longer exists there, the server's handlers mirror the 30 +protocol groups 1:1, the TUI runs on the protocol-derived client, and the product ships a +v1→v2 session-history migration. This SDK therefore generates the v2 protocol surface only; +the 1.x legacy surface is never built, and the generation target moves to the `v2` branch's +`packages/protocol/openapi.json`, pinned as a snapshot under `spec/` (the retarget executes +as the first task of the M1 callable-client arc). Public naming strips the `v2.` operationId +prefix and never bakes "V2" into type or client names (unchanged). This revises the +2026-08-08 both-surfaces decision in place — itself a revision of an earlier v2-only +position; the difference now is evidence, not taste: the both-surfaces premise ("the modern +block does not cover today's capability; the MCP-server goal needs all of it today") expired +when the v2 surface absorbed the capability gap (`mcp`, `config`, `vcs`, `project`, `shell`, +… are protocol groups now) and upstream's investment, distribution channels, and migration +tooling all converged on v2. Evidence: `docs/research/15-opencode-v2-platform.md`, research +log session 17; prior dated evidence: docs 09/10. + +## Consequences + +- The legacy hub, the legacy-marked sub-surface, the 16 stripped-name collisions, and + consumer-driven legacy testing all disappear; milestone M5 shrinks to completing the + generation profile over the single surface. +- Until upstream's v2 line reaches general availability the SDK targets a pre-release + server (`opencode2`; npm `@opencode-ai/cli@next`) — accepted: the M-series timeline runs + alongside upstream's stabilization, and the pinned-snapshot + fail-closed refresh + machinery exists for exactly this churn. +- The spec pin is a snapshot of a moving branch: refreshes stay deliberate (M2 boundary and + later), never HEAD-tracking. diff --git a/docs/adr/0006-monorepo-and-versioning.md b/docs/adr/0006-monorepo-and-versioning.md index c2b212b1e..0a53e4849 100644 --- a/docs/adr/0006-monorepo-and-versioning.md +++ b/docs/adr/0006-monorepo-and-versioning.md @@ -6,7 +6,7 @@ The MCP server is developed in this repository, not a separate one. It is by des adapter over our own SDK, and that architecture wants compile-time coupling: SDK breaking changes surface in the same CI run instead of after a publish (the cross-repo private-internals dependency was the failure mode that sank the unofficial `opencode-mcp`), -the consumer-driven legacy-test scope (ADR-0005) stays mechanically derivable, and the repo's +the MCP server's consumed-operation set stays mechanically derivable for test scoping, and the repo's infrastructure — analyzer wall, three-OS CI, real-process integration harness, docs discipline — is paid for once. diff --git a/docs/adr/0007-error-model.md b/docs/adr/0007-error-model.md index e9b9129a6..61455defc 100644 --- a/docs/adr/0007-error-model.md +++ b/docs/adr/0007-error-model.md @@ -1,12 +1,14 @@ # Error model: typed exception spine carrying tagged error data -Date: 2026-08-09 +Date: 2026-08-13 -All SDK failures surface through a typed exception spine (`OpenCodeException` → -`OpenCodeApiException` / `OpenCodeTransportException`); the spec's tagged error payloads -are generated as typed models under an `OpenCodeError` base and ride **as data** on the -exception, pattern-matchable without string sniffing. Channel selection is per call -site: throw by default, `NoThrow` opt-in per call, no client-level switch. This +API failures throw through a typed exception spine (`OpenCodeException` → +`OpenCodeApiException`) by default; per-call `NoThrow` returns them on the response spine. +Transport and protocol failures always throw `OpenCodeTransportException`. The spec's tagged +error payloads are generated as typed models under an `OpenCodeError` base and ride **as data** +on either channel, pattern-matchable without string sniffing. There is no client-level switch. Throwing API errors +retain the raw body on `OpenCodeApiException`; `NoThrow` responses retain it on their shared +response spine, including when a typed error cannot be parsed. This deliberately renders upstream's own idiom — tagged structural domain failures consumed as values — through .NET's nominal idiom instead; nothing in the taxonomy is lost, and the per-call `NoThrow` path preserves errors-as-values consumption where a call site diff --git a/docs/adr/0008-generated-operation-methods.md b/docs/adr/0008-generated-operation-methods.md index ca5dcb24b..ae0bcccd7 100644 --- a/docs/adr/0008-generated-operation-methods.md +++ b/docs/adr/0008-generated-operation-methods.md @@ -1,8 +1,8 @@ # All operation methods are generated; hand-wired constructs are fingerprint-pinned -Date: 2026-08-09 +Date: 2026-08-13 -Every operation method on both surfaces (61 modern + 127 legacy) is generator-emitted +Every operation method on the generated surface is generator-emitted as a one-line delegation into the hand-written behavior core; behavior (retry, error mapping, `NoThrow`, telemetry) never lives in method bodies. Hand-written op methods sit outside CI regen-verify and go silently stale as upstream moves — generated methods @@ -27,5 +27,9 @@ envelope; `text/*` → `string`); an unknown content type breaks generation. - The generator emits public API, so its emission rules (naming map, envelope payload names, guard clauses, XML docs) are review surface — curation config changes are API reviews. -- The bound-handle projection (spec §7.2) is one mechanical rule: operations with a - `{sessionID}` path parameter emit into `SessionClient`. +- Bound handles are opt-in group curation: a row declares a client name, handle name, + and required path parameter. The Binder applies one mechanical rule to that declaration: + operations carrying the parameter emit on the handle with it partially applied; + collection operations stay on the collection client. Groups without a handle declaration + stay flat. Emitters never branch on operation IDs, wire group names, or concrete client + names. diff --git a/docs/agents/handover-prompts/HANDOFF-2026-08-13-3.md b/docs/agents/handover-prompts/HANDOFF-2026-08-13-3.md new file mode 100644 index 000000000..a15624bf0 --- /dev/null +++ b/docs/agents/handover-prompts/HANDOFF-2026-08-13-3.md @@ -0,0 +1,75 @@ +# Post-M1 Handoff — Ship The Branch, Then Open M2 + +Date: 2026-08-13 + +Consume this document against live Git and delete it when the branch merges and the M2 batch +plan exists. It records the queue between M1 (complete) and M2 (not yet planned). + +## Onboarding Reading Order + +1. `AGENTS.md`, then ADRs 0003/0004/0005/0007/0008/0009. +2. `docs/ROADMAP.md` — M1 is complete; the status paragraph is the current-state summary. +3. Research log Sessions 17–18 — the retarget chain and the Arc B execution decisions + (mechanical payload names, User-Agent policy, route shape). +4. `tests/OpenCode.Sdk.Sandbox/README.md` — the committed debug playground. + +## Git And Workspace State + +| Item | State | +|---|---| +| Branch | `feature/m1-callable-client`, ~26 commits ahead of `origin/master`, never pushed | +| Gates | All six green at every commit; pack fails only on `.generation-incomplete` | +| Tests | 526 across tools + SDK legs (net8/9/10 locally; net472 only on Windows CI) | +| Demo evidence | `.scratchpad/m1-demo/DEMO.md` (gitignored) — paste into the PR body | + +Gate chains need `set -o pipefail` plus an explicit ALL-GREEN marker — a bare `| tail` +swallows a red gate's exit code. + +## Step 1 — Ship the branch + +1. **Multi-agent review (maintainer-triggered):** the maintainer runs `/code-review ultra` + on this branch — it is user-invoked and billed, an agent cannot start it; the no-arg form + bundles the local branch and needs no remote. Triage findings red-test-first; anything + touching public API names goes back to the maintainer before changing the baseline. +2. **Push + PR to `master`** (push only on the maintainer's explicit ask). PR body: the M1 + story, the `DEMO.md` transcript, and the pin-vs-installed version skew note + (`a6a712a` vs `opencode2` v0.0.0-next-17403 — deliberate snapshot policy). +3. **Watch first-contact CI.** This work has never seen the three-OS matrix. Known risk: the + Windows-only net472 test leg renders the shared `PublicApi.verified.txt` from embedded + (Polyfill-supplied) attributes — if it diverges, split the baseline per TFM or scrub the + attribute rendering; do not weaken the baseline test. `App.net472.config` must never flow + into the package when packaging is later enabled. + +## Step 2 — Open M2 (first breadth batch) + +Write the 1–2 page batch plan when the branch merges, not earlier (ROADMAP rule). The +standing proposal to put before the maintainer: **`v2.session.list` + `v2.message.list` + +`v2.session.get`** — it converts the demo's raw curl calls into SDK methods and pulls in the +real M2 machinery: + +- Query-parameter binding (drops the M1 `no query` wall) including optional parameters + (`limit`, `order`, `cursor`) and the deferred `location[...]` scoping decision. +- List payloads (`IReadOnlyList`) and the `{cursor, data}` / `{data, hasMore}` envelopes; + the cursor/paginator shape from the API design spec is a **public API review**. +- The `List*` verb rule joining `OperationNamePolicy` (collection GETs currently name as + `Get*`). +- Consider a deliberate pin refresh at this boundary (M2 boundary is the sanctioned moment; + drift lands as reviewable git diffs). + +## Step 3 — Horizon (order revisited at each milestone boundary) + +- **M3 streams:** SSE engine; `v2.session.log` (`after`/`follow`) semantics re-researched. +- **M4 launcher:** `OpenCodeServer.StartAsync` over `opencode2 serve`; discovery-file and + `--stdio` leash are candidate mechanisms. +- **M5 full surface:** complete profile, exclusion fingerprints (ADR-0008), packaging + unblocks. +- **M6 operational closure:** `refresh-spec`, Extensions DI (the sandbox grows its Generic + Host root then), retry/telemetry/hooks. +- Standing chores: the manual analyzer-package bump routine (nothing warns when it lags); + v2 GA watch (research doc 15's open items). + +## Stop Conditions + +Public-API taste decisions go to the maintainer as options with a recommendation. The Arc B +stop-condition list (binder needing raw OpenAPI, name-branching emitters, structural unions, +ownership ambiguity, TFM breakage, partial packs) carries into M2 unchanged. diff --git a/docs/research/00-research-log.md b/docs/research/00-research-log.md index 0938506a1..b11e84a5c 100644 --- a/docs/research/00-research-log.md +++ b/docs/research/00-research-log.md @@ -1,6 +1,6 @@ # Research log — 2026-08-08 -Date: 2026-08-11 +Date: 2026-08-13 > How this project's understanding was built: the questions asked, how each was > researched, what was found, and what decision or lesson came out of it. @@ -1469,3 +1469,191 @@ by hand against a real `opencode serve` — deliberately process-free (no launch harness, no CI leg; the demo output rides the PR description). Consumer-pull is the standing rule: every SpecIR fact, mechanism, and test names a consumer or a concrete failure it prevents. + +# Session 16 — 2026-08-13: M1 Arc B planning + +## Q66: Is the session handle a mechanical fact or an API-policy decision? + +**Found:** OpenAPI says only that `sessionID` is a required path parameter. It cannot imply +that consumers should obtain a `SessionClient`, keep the ID on that handle, or omit the ID from +operation signatures. Hard-coding `{sessionID}` or `SessionClient` in an emitter would make +curation incomplete and turn breadth into endpoint-specific branches. + +**Decision (maintainer):** modern group curation may declare the collection client, bound +handle, and handle path parameter. The Binder applies that declaration as one general +partial-application rule; emitters consume final plans and know no operation IDs, wire groups, +or concrete clients. The legacy surface remains flat. + +## Q67: Which acronym casing should generated C# identifiers use? + +**Found:** the prior two-letter-uppercase rule emits `ID`, `SessionID`, and `CallID`, while the +maintainer wants one predictable PascalCase convention and the first API baseline has not yet +shipped. Wire fidelity is independent because `[JsonPropertyName]` retains the source spelling. + +**Decision (maintainer):** normalize acronym tokens with ordinary PascalCase regardless of +length (`Id`, `SessionId`, `CallId`, `Url`, `Api`); exceptional brand spelling remains curated. +Apply the change before approving the first public API baseline. + +## Q68: Should optional collection properties become nullable to model deserializer input? + +**How researched:** checked `SessionMessageAgentSwitched.metadata` in the pin and ran a +source-generated System.Text.Json 10.0.11 probe with `RespectNullableAnnotations=true`, init-only +record properties, `[AllowNull]`, and a nullable-input normalization helper. + +**Found:** optionality and nullability are independent. The pin omits `metadata` from `required` +but declares `type: object`, so absence is valid and explicit null is not. STJ's record path may +feed an absent optional collection through the init path as null; `[AllowNull]` removes the C# +warning but also admits explicit JSON null. A non-null property delegating to a nullable-input +helper preserves empty-on-absence while STJ rejects explicit null before assignment. + +**Decision (maintainer):** optional non-null collections remain non-null in public C#. Normalize +absent values to immutable empty collections through an internal nullable-input helper, preserve +defensive copies, and reject explicit null unless the schema itself admits null. + +# Session 17 — 2026-08-13: v2 retarget research + +The maintainer opened the direction question before Arc B implementation: is the dual-surface +v1 investment worth its complexity while upstream's investment moves to v2? Live inspection of +`anomalyco/opencode` branch `v2` (head `1288161`, retrieved 2026-08-13) via shallow clone, +GitHub API, npm registry dist-tags, and `update.opencode.ai` probes, plus a live install and +run of the v2 CLI on this machine. Platform detail is canonical in +`15-opencode-v2-platform.md`; decisions were sealed with the maintainer in-session and the +canonical documents corrected in the same change. This entry is the chain. + +## Q69: Is the dual-surface v1 investment still justified given upstream's v2 line? + +**How researched:** the v2 branch tree read at file level (server handlers, protocol groups, +TUI dependencies, CLI command specs, desktop wiring); commit cadence via the GitHub API; the +ROADMAP v2-watch item's session-12 censuses re-run against today's head. + +**Found:** the legacy surface's source is gone — `packages/opencode` does not exist on the v2 +branch; the restructured monorepo (`core`/`server`/`client`/`protocol`/`sdk-next`) serves only +the protocol surface (30 server handler files ↔ 30 protocol groups, 1:1). The TUI runs on the +protocol-derived `@opencode-ai/client`, not the dual-generation `@opencode-ai/sdk`. A v1→v2 +session-history migration is being built in code (`v2.experimental.migration.v1.status`: +"Return the progress of the V1 to V2 session history migration"). Commit volume on 2026-08-13 +alone: 15 (features, fixes, tests, docs), while `dev` — the default branch — carries 1.x +maintenance. The two-endpoint-surface problem the SDK's dual-surface design absorbs is a +v1-line artifact; v2 has one surface. + +**Decision (maintainer, sealed): retarget — the SDK targets the v2 protocol surface only; the +legacy surface is never built.** ADR-0005 is revised in place (the ADR-0003 precedent), not +superseded. M1 Arc B continues with a prepended retarget task (pin swap + wall admit + +regeneration). The staged alternative — finish Arc B on the 1.18.15 pin, retarget at the M2 +boundary — was weighed and rejected: after the live-server demonstration (Q72) removed the +"no real v2 server to demo against" cost, staging would only buy double generation, a double +API-baseline review, and a walking-skeleton demo against a surface already declared dead. + +## Q70: What is the v2 spec artifact, and how far has the surface moved since our pin? + +**Found:** `packages/protocol/openapi.json` — 104 paths / 120 operations / 324 schemas, all +mounted under `/api`, operationIds still `v2.`-prefixed (the prefix-strip naming policy +carries over). The artifact is maintained: `generate-openapi.ts --check` is upstream's own +regen-verify, "chore: generate" commits land daily, and the CLI's `api` command resolves +OpenAPI operationIds against it. Versus the pinned 61-op modern block: 8 operations dropped — +including the durable-stream trio `v2.session.events` / `v2.session.history` / +`v2.session.messages` — and 67 added; the new families (`mcp` 6, `config`, `vcs` 3, +`project` 3, `shell` 6, `websearch`, `generate`) absorb what doc 10 enumerated as the 78-op +legacy capability gap, and `tui.*` is gone as a remote-control surface. Dialect census: 420 +`allOf` occurrences, every one a single-element validation-only wrapper; 0 `const` (literals +back to single-value `enum`, 370 sites); 446 `anyOf`, 0 `discriminator` — the union machinery +and ADR-0009 carry over unchanged; dotted component names; `v2.session.log` (`after` + +`follow` parameters) replaces the durable stream; `v2.pty.connect` carries `x-websocket`. + +**Decision:** the wall admit rule for the single-element validation-only `allOf` wrapper class +lands in the Arc B retarget task; the M3 stream design is re-derived against `session.log` +when M3 starts. + +## Q71: Does opencode v2 ship its own MCP server? + +**Found:** no. The protocol `mcp` group (list/add/remove/connect/disconnect/resource.catalog) +manages *configured* MCP servers — opencode as MCP host/client (`@modelcontextprotocol/sdk` +1.29.0; `packages/core/src/mcp/{client,stdio,oauth}.ts` are all client-side). The +self-exposure protocol upstream chose is ACP, not MCP: `opencode acp` — "Start an Agent +Client Protocol server" — targets editor embedding. + +**Decision:** the MCP-server premise (research docs 03/04) stands on v2 — the gap this +repository's second deliverable fills remains open. ACP is recorded as an adjacent surface to +watch: it may absorb some drive-opencode-from-another-agent use cases. + +## Q72: How is v2 distributed, and what is its server model? + +**How researched:** npm registry dist-tags for the scoped packages; `update.opencode.ai` +channel feeds; GitHub releases of `anomalyco/opencode-beta`; CLI/client/desktop source read at +file level; then a live install and run on this machine. + +**Found:** distribution is two-channel and bypasses the old `opencode-ai` package: the CLI +ships as `@opencode-ai/cli@next` (0.0.0-next-17403, published 2026-08-13), installing +side-by-side as `opencode2`; the desktop beta rides `update.opencode.ai` (channel feeds, +OIDC-authenticated publish) over `anomalyco/opencode-beta` GitHub releases +(0.0.0-beta-17406, same day), bundling its own CLI. Installation starts no server. The server +model is a shared per-user background service: a 0600 registration file in the XDG state +directory (`{id, version, url, pid, password}`) is the complete discovery contract; +`Service.ensure` reuses a healthy compatible server, replaces a version-mismatched one, and +otherwise spawns detached contenders until one wins registration. Explicit modes: `serve` +(foreground), `serve --service` (managed daemon, `service start/stop/status/restart`), +`serve --stdio` (embedding: JSON `{url}` handshake on stdout, stdin as liveness leash), +`--standalone` (private server), `--server ` (remote). `pair` prints server URLs + Basic +username `opencode` + password (+ QR) — the v1 auth model unchanged. Live verification on +this machine: `opencode2 service status` → `stopped` after install; `opencode2 serve +--hostname 127.0.0.1 --port 41999` up; unauthenticated `GET /api/health` → 401; +authenticated → 200 `{"healthy":true,"version":"0.0.0-next-17403","pid":…}`; +`GET /api/session?limit=2` → `{data:[…]}` containing this machine's real session history (the +central store reads existing opencode data). + +**Decision:** the launcher milestone (M4) maps to `opencode2 serve`; connect-or-launch fits +the discovery-file contract; whether the SDK adopts `location[...]` query addressing in place +of the v1 `x-opencode-directory` header is decided inside the retarget task. + +**Documentation pass (this session):** `15-opencode-v2-platform.md` created as the canonical +v2 platform picture; docs 09/10 corrected in place (resolved UNVERIFIED items, stamped +consequence updates); ADR-0005 revised in place; `AGENTS.md`, `docs/ROADMAP.md`, and the Arc B +plan updated to the single-surface v2 target. + +# Session 18 — 2026-08-13: M1 Arc B execution decisions + +An implementation session, not a research sweep: Tasks 1–4 of the Arc B plan landed and M1 +closed with a live demonstration. This entry records only the decisions sealed along the way; +execution detail lives in git and the (now consumed) Arc B plan checkboxes. + +## Q73: Are envelope payload names curated per operation or derived mechanically? + +**How researched:** the sealed API design (payload names from a fail-closed curation map, +§5.1/§8.5) collided with the maintainer's retarget-era "no taste curation" convention when the +bare `ServiceHealth` payload needed a name; both models were priced against the ~100 +payload-carrying operations of the full v2 surface. + +**Found:** the mechanical candidate — the operation's subject tokens (identifier segments +after the group that do not restate the HTTP method; the group name when none remain) — +reproduces both sealed M1 values exactly (`Message`, `Health`), and a fail-closed collision +check against the response spine (`Error`, `IsError`, `RawBody`, `Status`, the response type +name) preserves the forced-review property for the cases that actually need a human. + +**Decision (maintainer, sealed):** payload names derive mechanically; +`envelopePayloadNames` demotes to an override ledger for spine collisions and C#-illegal +results only; a collision fails generation until an override lands. The required-per-operation +validation retired with it. + +## Q74: What may the User-Agent version token contain when the informational version is unusable? + +**Found:** SourceLink appends `+` to `AssemblyInformationalVersionAttribute`; a +missing or unparsable attribute is possible in exotic build contexts, and the handoff banned +a silent assembly-version substitute. + +**Decision (maintainer, sealed):** strip build metadata after `+`; when the remainder is +missing or fails `ProductHeaderValue` parsing, omit the version token entirely +(`OpenCode.Sdk` alone) — never a silent fallback, never a construction failure +(`UserAgentPolicy`). + +## Q75: What shape do generated route members take? + +**Decision (maintainer, sealed):** `OpenCodeRoutes` nests one static container per operation +group (client name, or the Pascal group for root placement); parameterless operations emit a +`const`, parameterized ones a `Template` const plus an escaped `(...)` +builder; members never restate their container (`Health.Get`, `Sessions.GetMessage`). CA1034 +is arbitrated for the one generated file. Route member names ride the plan +(`RouteContainerName`/`RouteMemberName`) so emitters stay name-blind. + +**Documentation pass (this session):** ROADMAP moved to M1-complete/M2-next; the Arc B plan's +checkboxes closed in the same commits as their code; the consumed handover was deleted and a +fresh one (`HANDOFF-2026-08-13-3.md`) records the ship/M2/horizon queue. diff --git a/docs/research/09-upstream-v1v2.md b/docs/research/09-upstream-v1v2.md index da528b633..8cb35351f 100644 --- a/docs/research/09-upstream-v1v2.md +++ b/docs/research/09-upstream-v1v2.md @@ -67,7 +67,9 @@ most of the TUI. 1. Both surfaces of the pinned 1.x spec are generated: the MCP-server goal needs today's full capability, and the modern block alone does not provide it. Deep integration testing - targets the modern surface; legacy is best-effort. + targets the modern surface; legacy is best-effort. (2026-08-13: ADR-0005 revised in place + — the SDK targets the v2 protocol surface only and the legacy surface is never built; + current v2-line state: `15-opencode-v2-platform.md`.) 2. Public naming must not bake in the transitional `v2.` prefix — it does not exist in 2.0; the 2.0 rename wave is absorbed at a major release (evolve/deprecate on the evidence then). 3. Generator: parse `const` alongside single-value `enum`, and mangle dotted schema names. @@ -78,7 +80,8 @@ most of the TUI. ops; not inspected individually). - Whether a migration guide exists outside the repo (blog/Discord) — none found in docs/README; GitHub issue/discussion search failed this session (API 422), so maintainer - statements there are unchecked. + statements there are unchecked. (2026-08-13: official v2 documentation now exists at + opencode.ai/v2/docs.) - The exact 61 → 112 operation rename mapping (spot-checked only; a full side-by-side diff belongs to the API design session). - How long 1.x releases will keep publishing the `v2.*` `/api` projection. diff --git a/docs/research/10-v2-to-2.0-operation-mapping.md b/docs/research/10-v2-to-2.0-operation-mapping.md index 2b9f4b87f..a409f57a2 100644 --- a/docs/research/10-v2-to-2.0-operation-mapping.md +++ b/docs/research/10-v2-to-2.0-operation-mapping.md @@ -213,11 +213,15 @@ separate from the v2 durable session stream; relation unexplored.) protocol package's — i.e. the prefix-stripped `v2.*` names our ADR-0005 policy already adopts. The genealogy correction *supports* the locked naming decision and removes the "absorb the 2.0 rename wave" pressure doc 09 implied: there is no verified rename wave to - absorb, because no authoritative next-major spec exists. + absorb, because no authoritative next-major spec exists. (2026-08-13: one now exists — the + active `v2` branch publishes `packages/protocol/openapi.json`, a continuation of the same + protocol family with `v2.`-prefixed operationIds; see `15-opencode-v2-platform.md`.) 2. **ADR-0005's deletion premise needs re-grounding, not reversal.** "At opencode 2.0 the legacy area is deleted" was partly argued from the 2.0-branch spec; that evidence is void. The live signals still point the same way (TUI migrating to `sdk.client.v2.*`, deprecation markers, protocol extraction) but the shape and date of the next major are now UNVERIFIED. + (2026-08-13: resolved — ADR-0005 revised in place: the SDK targets the v2 protocol surface + only; the legacy surface is never built.) 3. **The capability gap is precisely enumerated.** The 78-op list is what the MCP server and any full-capability consumer must take from the legacy sub-surface today (`tui.*` 13, `mcp.*` 8, session-editing 16, config/auth/project/vcs/find/lsp/etc.), plus the 17 @@ -229,14 +233,19 @@ separate from the v2 durable session stream; relation unexplored.) 5. **The durable session stream is protocol-only.** `v2.session.events`/`v2.session.history` + `SessionDurableEvent` (the spec's single `oneOf`) have no ancestor/root counterpart — the locked SSE-resume design (`after` cursor) rides the newest, least-proven part of the - surface. + surface. (2026-08-13: the trio was since dropped on the `v2` branch in favor of + `v2.session.log` (`after`/`follow`) and cursor-paged `v2.message.list`; the M3 stream + design is re-derived against that surface — research doc 15.) 6. **`pty.connect` needs an explicit decision:** exclude from generation, or generate the fake GET and pair it with hand-written WebSocket support (upstream ships the fake GET). ## UNVERIFIED / open - Whether any newer next-major branch or spec exists beyond `dev` (branch list scanned; - `app-v2-only`, `tui-v2`, `app-backend-v2` and similar branches not inspected). + `app-v2-only`, `tui-v2`, `app-backend-v2` and similar branches not inspected). (2026-08-13: + answered — the active successor is branch `v2`, diverged from `dev` 2026-06-26 with daily + commits; its spec lives at `packages/protocol/openapi.json`; see + `15-opencode-v2-platform.md`.) - The exact mechanism applying the `v2.` operationId prefix and `/api` mount to `ServerApi` (`api.ts`/`public.ts` read in part; `@opencode-ai/protocol/api`'s `makeApi` not traced). - Why root spec groups like `vcs`/`find`/`auth`/`command` have no matching file under diff --git a/docs/research/11-error-channel-scope.md b/docs/research/11-error-channel-scope.md index 7bed62eee..fa34a2087 100644 --- a/docs/research/11-error-channel-scope.md +++ b/docs/research/11-error-channel-scope.md @@ -1,6 +1,6 @@ # Error-channel scope: per-call NoThrow only, or a client-level default too? -Date: 2026-08-09 +Date: 2026-08-13 > Research snapshot, 2026-08-09. Question: given the sealed throw-by-default error model > (typed `OpenCodeException` spine, per-call `OpenCodeRequestOptions.ErrorBehavior`), @@ -15,7 +15,7 @@ Date: 2026-08-09 ## 1. The question Every operation throws typed exceptions by default and returns a typed envelope -(`Status`/`IsError`/`Error` + guarded payload getter); a per-call +(`Status`/`IsError`/`Error`/`RawBody` + guarded payload getter); a per-call `OpenCodeRequestOptions { ErrorBehavior = Default | NoThrow }` exists. The only open point is scope: is per-call the *only* channel switch, or does `OpenCodeClientOptions` get an `ErrorBehavior` default (per-call still overriding)? @@ -165,7 +165,7 @@ Against (per-call only): - **DI sharing.** One registration serves many consumers; a library or second team member resolving `OpenCodeClient` inherits a silently flipped contract. None of the surveyed .NET SDKs make this possible. -- **The 204 exposure.** 24 of 61 modern ops return 204 (design spec §6): under NoThrow +- **The 204 exposure.** 19 of 61 modern ops return 204 (design spec §6): under NoThrow there is no payload getter to trip, so an unchecked failure is *completely* silent. A client-level NoThrow default converts this from a per-call, locally visible risk into a config-at-a-distance one. @@ -177,8 +177,8 @@ For (client-level default): - **Dogfood ergonomics.** The MCP server maps failures to tool-error results uniformly; per-call-only means `OpenCodeRequestOptions.NoThrow` on nearly every call. Upstream - felt the mirror-image pain: the TUI sprinkled 113 `throwOnError: true` call sites to - escape its result-default (design spec §6). + felt the mirror-image pain: its app/CLI layers carry approximately 76 non-generated + `throwOnError: true` call sites to escape result-default behavior (design spec §6). - **Upstream symmetry.** The JS SDK has the client-level knob and upstream's app uses it — but §2 shows it is inherited hey-api machinery, opposite in direction, and type-guarded in a way C# cannot replicate. The symmetry is superficial. @@ -190,7 +190,7 @@ For (client-level default): per-call/per-signature scope; the only client-level precedents run in the safe (escalating) direction on response-default clients; FDG explicitly forbids option-conditional throwing, and the per-call form is the ecosystem's tightly contained -deviation from that rule. The 24×204 silent-failure surface makes the suppressing +deviation from that rule. The 19×204 silent-failure surface makes the suppressing direction uniquely dangerous for this API, and per-call-only is the extend-only choice. Middle path, if MCP-server ergonomics prove painful in practice: a **scoped no-throw diff --git a/docs/research/14-polyfill-capability-inventory.md b/docs/research/14-polyfill-capability-inventory.md new file mode 100644 index 000000000..b9c301c3a --- /dev/null +++ b/docs/research/14-polyfill-capability-inventory.md @@ -0,0 +1,211 @@ +# Polyfill capability inventory + +Date: 2026-08-12 + +This inventory answers only what the pinned Polyfill package contributes to this repository, +which parts are actually enabled, and what that means for M1's minimal HTTP transport. It is +not a general catalogue of every API the package can emit. + +## Q1: What package is wired into the repository, and how does it reach product code? + +### Finding + +`Directory.Packages.props` pins Polyfill `11.0.2`. `Directory.Build.props` references it for +every project and every target framework with `PrivateAssets=all`; the product matrix remains +`netstandard2.0;net472;net8.0;net9.0;net10.0`. + +Polyfill is a source-only development dependency. Its NuGet package contains target-specific +C# files under `contentFiles/cs//` plus `build/Polyfill.targets`; NuGet adds those files as +hidden `Compile` items with `Pack=false`, and the target removes disabled optional groups. No +Polyfill runtime assembly is loaded and no Polyfill package dependency flows to an SDK +consumer. The selected source is compiled into each `OpenCode.Sdk` target instead. + +A restored-project query after optional-group removal reported these Polyfill compile-item +counts for `OpenCode.Sdk`: + +| Target | Polyfill source items | +|---|---:| +| `netstandard2.0` | 257 | +| `net472` | 256 | +| `net8.0` | 85 | +| `net9.0` | 53 | +| `net10.0` | 36 | + +The counts describe pinned package source inputs, not public SDK APIs and not an evergreen +test assertion. A source item may still contain target- or reference-dependent `#if` blocks. + +### Decision + +Keep the package repository-wide and private. Treat its selected source as part of each built +SDK assembly, not as a runtime dependency or a substitute for multi-TFM verification. + +## Q2: Which optional capabilities are actually enabled? + +### Finding + +The repository sets only `PolyArgumentExceptions=true` globally. The package target makes the +other optional groups opt-in by removing their source when the corresponding property is not +`true`. + +| Capability | Repository setting | Effective state | +|---|---|---| +| Core target-specific polyfills | Always selected by the package | Enabled | +| `PolyArgumentExceptions` | `true` | Enabled where the target lacks a BCL overload | +| `PolyEnsure` | Unset | Disabled | +| `PolyGuard` | Unset | Disabled | +| `PolyNullability` | Unset | Disabled | +| `PolyStringInterpolation` | Unset | Disabled | +| `PolyPublic` | Unset | Polyfill types remain internal | +| `PolyUseEmbeddedAttribute` | `true` in the SDK and tooling projects | Both declare friend test assemblies that compile their own Polyfill copy; embedded types stay hidden from them | +| `AllowUnsafeBlocks` | Unset | Unsafe-only overload bodies are not enabled | + +`tests/Directory.Build.props` also disables TUnit's separate polyfill injection because the +repository-wide reference already supplies the needed sources. This prevents duplicate package +items; it does not disable the root Polyfill reference. + +On `netstandard2.0` and `net472`, the enabled argument-exception group supplies modern throw +helpers for `ArgumentException`, `ArgumentNullException`, `ArgumentOutOfRangeException`, and +`ObjectDisposedException`. On `net8.0`, the package contains only the unsafe pointer form of +`ArgumentNullException.ThrowIfNull`, whose body is excluded here because unsafe blocks are off. +The normal modern targets use their BCL implementations. + +### Decision + +Use BCL throw-helper syntax uniformly in hand-written Arc B code. Do not enable another +optional Polyfill group without a concrete product-code consumer and a multi-TFM build proving +that the added source is necessary. + +## Q3: Which language and runtime gaps relevant to the SDK are covered per target? + +### Finding + +Polyfill contributes two different kinds of compatibility: + +1. Compiler-recognized metadata types make modern C# source legal on older reference + assemblies. The downlevel sets include `IsExternalInit`, `RequiredMemberAttribute`, + `CompilerFeatureRequiredAttribute`, and `SetsRequiredMembersAttribute`, enabling records, + `init`, and `required` metadata on `netstandard2.0` and `net472`. +2. Executable extension/helper code implements newer BCL-shaped APIs over older primitives. + Examples relevant to transport include `Task.WaitAsync`, cancellation-token overloads on + `HttpContent`, and memory-based stream overloads. + +The first category does not install a newer CLR or emulate new runtime semantics. It supplies +the metadata names expected by the compiler. The second category is real code in the SDK +assembly and can have implementation costs or weaker cancellation than a native API. + +After `ResolveAssemblyReferences`, the current project graph produces the following Arc B +capabilities: + +| Target | Language metadata | Guard helpers | Async/stream bridge | HTTP bridge | +|---|---|---|---|---| +| `netstandard2.0` | Downlevel `init`/`required` types | Full enabled group | `Task.WaitAsync`; memory/value-task stream APIs are enabled through the `System.Text.Json` dependency graph | `FeatureHttp` is active; `HttpClient`/`HttpContent` compatibility extensions compile | +| `net472` | Downlevel `init`/`required` types | Full enabled group | `Task.WaitAsync`; memory/value-task stream APIs are enabled through the `System.Text.Json` dependency graph | HTTP source files are present, but `FeatureHttp` is currently inactive because `System.Net.Http` is absent from `ReferencePath` | +| `net8.0` | Native BCL/compiler support | Native for the safe overloads | Native for Arc B needs | Only the package's cancellation-aware `HttpContent.LoadIntoBufferAsync` wrappers remain | +| `net9.0` | Native | Native | Native for Arc B needs | No Arc B-specific HTTP compatibility source | +| `net10.0` | Native | Native | Native for Arc B needs | No Arc B-specific HTTP compatibility source | + +Polyfill detects capabilities from the assemblies actually passed to the compiler. It does not +provide `System.Net.Http` itself. The current `net472` reference graph therefore cannot compile +the forthcoming `HttpClient` transport merely because `Polyfill_HttpClient.cs` exists in the +package. M1 Arc B must add the .NET Framework `System.Net.Http` reference and verify that +`FeatureHttp` becomes active where a selected compatibility overload is used. + +### Decision + +Continue writing the generated model and client surface with the pinned C# language features. +For Arc B, add and verify the missing `net472` framework HTTP reference; do not add a second +HTTP implementation package merely to obtain APIs already present in the framework plus the +source polyfills. + +## Q4: Are the polyfilled async and HTTP signatures behaviorally identical to native APIs? + +### Finding + +Not always. The package implements missing APIs using the primitives available on each target: + +- Downlevel `HttpContent.ReadAsStreamAsync(CancellationToken)` calls the older parameterless + method and then waits through the polyfilled `Task.WaitAsync`. +- The downlevel `Task.WaitAsync` races the original task against cancellation/timeout. It stops + the caller's wait but cannot cancel an underlying operation that did not accept the token. +- Memory-based stream overloads may adapt through arrays or pooled buffers. Their signatures + improve source compatibility but do not guarantee the allocation profile of a native modern + runtime implementation. +- The polyfilled synchronous `HttpClient.Send` blocks over `SendAsync`; the SDK has no sync + operation surface and has no reason to use it. +- The polyfilled `HttpClient.Get*Async` conveniences own status handling themselves, which is + incompatible with the SDK's typed success/error envelopes and explicit response ownership. + +These are valid compatibility implementations, but API-shape parity is not proof of transport +policy parity. + +### Decision + +Arc B should use `HttpClient.SendAsync(HttpRequestMessage, +HttpCompletionOption.ResponseHeadersRead, CancellationToken)` as its primitive, pass the token +to the actual send, and own request/response disposal. It should not build the behavior core on +Polyfill's synchronous send or `Get*Async` convenience methods. If a downlevel content API can +only cancel the wait, response disposal and cancellation tests must prove the SDK's observable +contract. + +## Q5: What does Polyfill not provide for M1 Arc B? + +### Finding + +Polyfill closes API availability gaps only. It does not implement any opencode SDK policy: + +- endpoint validation or URI construction; +- route-parameter escaping; +- owned versus injected `HttpClient` lifetime; +- password/environment resolution or Basic authentication; +- User-Agent and `x-opencode-directory` decoration; +- source-generated `System.Text.Json` selection or reflection-fallback prevention; +- success-envelope construction; +- status-to-generated-error mapping and unknown-error preservation; +- `OpenCodeException`, `OpenCodeApiException`, or `OpenCodeTransportException` classification; +- throw-by-default versus per-call `NoThrow` behavior; +- cancellation preservation, response disposal, retry, telemetry, hooks, or SSE semantics; +- Native AOT compatibility guarantees. + +`System.Text.Json` `10.0.11`, not Polyfill, supplies the serializer and brings several downlevel +support assemblies that incidentally activate memory/value-task feature flags in Polyfill. + +### Decision + +Keep all listed behavior in the hand-written runtime core and generated operation layer defined +for M1. Polyfill is an implementation aid at the BCL boundary; it must never become an +architectural collaborator or a reason to weaken transport/error tests. + +## Q6: What bounded risks remain? + +### Finding + +- Source-only means no runtime package dependency, but it does not mean zero assembly impact; + selected implementations are compiled into each target. +- Upstream recommends targeting every framework consumers are expected to run for best + performance. The locked repository matrix intentionally uses bridge targets rather than every + intermediate TFM, so a newer consumer may select an older asset carrying extra internal + implementations. +- Package updates can change implementation behavior as well as source availability. The + package target also warns against replacing `DefineConstants` from the command line because + doing so can erase its feature flags. +- `netstandard2.0` has no runtime of its own. Its Polyfill-assisted compile path still receives + runtime proxy coverage through the locked `net472` legs. + +### Decision + +Do not change the TFM matrix or analyzer policy from this inventory. Review Polyfill behavior +when its pinned version changes, append rather than replace `DefineConstants`, and let Arc B's +multi-TFM build plus `net472` behavior tests arbitrate the compatibility path. + +## Evidence + +- Repository: `Directory.Build.props`, `Directory.Packages.props`, + `src/OpenCode.Sdk/OpenCode.Sdk.csproj`, `tools/OpenCode.Sdk.Tools/OpenCode.Sdk.Tools.csproj`, + and `tests/Directory.Build.props`. +- Restored Polyfill `11.0.2` package: `polyfill.nuspec`, `build/Polyfill.targets`, and the + target-specific files under `contentFiles/cs/`. +- MSBuild evaluation: `dotnet msbuild src/OpenCode.Sdk/OpenCode.Sdk.csproj + -p:TargetFramework= -getItem:Compile` and feature resolution after + `ResolveAssemblyReferences` for every repository target. +- [Polyfill 11.0.2 on NuGet](https://www.nuget.org/packages/Polyfill/11.0.2). +- [Polyfill source and documentation](https://github.com/SimonCropp/Polyfill/tree/11.0.2). diff --git a/docs/research/15-opencode-v2-platform.md b/docs/research/15-opencode-v2-platform.md new file mode 100644 index 000000000..2f5e1f6aa --- /dev/null +++ b/docs/research/15-opencode-v2-platform.md @@ -0,0 +1,262 @@ +# The opencode v2 platform: branch, surface, architecture, distribution + +Date: 2026-08-13 + +> Retarget-decision research (docs 09/10 follow-up). Question: what is the opencode v2 platform +> today — which branch carries it, what API surface it serves, how its server/client architecture +> and its distribution work, and what retargeting this SDK onto it changes. Sources, all retrieved +> 2026-08-13: `anomalyco/opencode` branch `v2` at head `1288161` ("chore: generate"), inspected +> via shallow clone; the GitHub REST API (branch head, commit list, releases, default branch); the +> npm registry (`@opencode-ai/cli` and `opencode-ai` dist-tags); live probes of +> `update.opencode.ai`; and a live install and run of `@opencode-ai/cli@next` (0.0.0-next-17403) +> on this machine. This document is the canonical current-state picture of the v2 platform; docs +> 09 and 10 remain the dated v1-line genealogy it extends, and research log session 12 (Q58) +> recorded the branch's first sighting. + +## 1. The active branch is `v2`, and the investment is visibly there + +- Branch head `1288161` was committed 2026-08-13T07:18:58Z ("chore: generate" — the spec + artifact regenerating, §3). **15 commits landed on 2026-08-13 alone**, a full-width mix: + `fix(app): unify v2 server and session lifecycle`, `feat(app): redesign non-modal settings`, + `ci: make v2 publishing manual`, `feat(deskltop): use opencode2 in WSL`, core/tui fixes, + test refactors, docs, and two separate `chore: generate` runs. +- The **default branch remains `dev`** — the 1.x maintenance line, which released v1.18.18 on + 2026-08-13T01:15Z. Every GitHub release on the main repository is still a `v1.18.x` tag. +- The monorepo is restructured: ~37 packages (`core`, `server`, `client`, `protocol`, `cli`, + `sdk-next`, `desktop`, `tui`, `schema`, `util`, `updates`, `httpapi-codegen`, …). + **`packages/opencode` — the source of the 127-operation legacy root block — does not exist + on the branch**, and neither does `packages/sdk`. +- v1's end is written in code: `packages/protocol/src/groups/migration.ts` declares + `v2.experimental.migration.v1.status` — *"Return the progress of the V1 to V2 session history + migration."* — with a `V1MigrationStatus` union (`required`/`completed`, `running` + progress, + `error`). v2 absorbs v1 installations in place; the v1 line is being sunset, not forked from. + +Unlike the frozen April `2.0` branch (doc 10's genealogy correction), this branch shows every +liveness signal that one lacked: daily volume, a regenerating committed artifact, migration +machinery, and a client stack that has already moved (§2). + +## 2. One surface — the dual-surface problem does not exist on v2 + +- `packages/server/src/handlers/` and `packages/protocol/src/groups/` are **1:1 mirrors — 30 + files each** (agent, command, config, credential, debug, event, form, fs, generate, health, + integration, location, mcp, message, migration, model, permission, plugin, project, + project-copy, provider, pty, question, reference, server, session, shell, skill, vcs, + websearch). The server serves the protocol surface and nothing else; no legacy route layer + exists anywhere on the branch. +- The TUI depends on `@opencode-ai/client` — whose `contract.ts` is one line, re-exporting + `ClientApi` from `@opencode-ai/protocol/client` — not on the old `@opencode-ai/sdk`. The + client ships `effect/` and `promise/` flavors over the same protocol contract, plus the + background-service discovery machinery (§6). +- `sdk-next` is a separate concern: an in-process embedding host, not the network client — + its README: *"The SDK executes Server's assembled HTTP router in memory. It opens no listener + and performs no network I/O"* and *"This transitional package will replace the existing + generated `@opencode-ai/sdk` after its consumers migrate."* + +v1's defining problem — two generations served simultaneously, with upstream's own TUI calling +both — is a v1-line artifact. On v2, server, client, TUI, and desktop all speak one surface. + +## 3. The spec artifact is maintained, checked, and self-consumed + +`packages/protocol/openapi.json` (681 KB): OpenAPI **3.1.0**, title "opencode HttpApi", info +version `0.0.1`, self-described *"Experimental HttpApi surface for selected instance routes."* +**104 paths / 120 operations / 324 schemas**, every path under `/api`, operationIds still +`v2.`-prefixed (the prefix-strip naming policy carries over unchanged). + +The artifact is not a dump: `packages/protocol/package.json` owns +`"generate": "bun run script/generate-openapi.ts"` and `"check:generated": "… --check"` — their +own regen-verify — and the branch head itself is a `chore: generate` commit. Their own CLI +consumes it: `opencode2 api` takes *"OpenAPI operation ID, or an HTTP method followed by a +path"*. First-class producer, first-class consumer — low rot risk for anyone generating from it. + +## 4. Surface diff against our pinned v1.18.15 modern block: 61 → 120 + +Eight pinned-block operations are gone: `v2.integration.attempt.cancel` / `.complete` / +`.status`, `v2.integration.connect.oauth` (the attempt/connect model reshaped into +`integration.command.*` ×3 + `integration.oauth.*` ×4), `v2.pty.connectToken` (now +`v2.pty.connect.token`), and — significant for M3 — `v2.session.events`, `v2.session.history`, +`v2.session.messages` (§5, stream reshape). + +67 operations are new. By family: `session.*` grew from 25 to **48** (forms ×6, inbox ×4, +instructions ×3, plus `export`/`import`/`fork`/`rename`/`move`/`remove`/`shell`/`skill`/ +`synthetic`/`log`/`command`/`background`/`generate`); **`mcp.*` ×6** (add, connect, disconnect, +list, remove, resource.catalog); **`shell.*` ×6**; `integration.*` reshaped to 10; +**`vcs.*` ×3**; `project.*` ×3; `websearch.*` ×2; `debug.*` ×2; plus `config.get`, `agent.get`, +`model.default`, `plugin.list`, `server.get`, `health.stop`, `generate.text`, `form.request.list`, +`message.list`, `experimental.migration.v1.status`, `experimental.integration.wellknown.add`. + +| Family | Ops | Family | Ops | +|---|---|---|---| +| session | 48 | fs, permission, project, projectCopy, vcs | 3 each | +| integration | 10 | agent, credential, debug, experimental, health, model, provider, websearch | 2 each | +| pty | 7 | command, config, event, form, generate, location, message, plugin, question, reference, server, skill | 1 each | +| shell, mcp | 6 each | | | + +The consequence for doc 10's **78-operation capability gap**: it is being absorbed into the +protocol surface. `mcp`, `config`, `vcs`, `project` — the families that forced ADR-0005's +"generate both surfaces" — now exist as modern operations, joined by new capability (`shell`, +`websearch`, `generate`). The 13-operation `tui.*` remote-control surface is **gone entirely** — +not migrated, removed. + +## 5. Dialect census (head `1288161`) + +| Construct | Count | Note | +|---|---|---| +| `allOf` | 420 | **100% single-element**, validation-only wrappers (`{"type":"integer","allOf":[{"exclusiveMinimum":0}]}`) | +| `const` | 0 | the April const shift stays reverted | +| single-value `enum` | 370 | the literal-marker dialect, unchanged | +| `anyOf` | 446 | discriminator-free unions, unchanged | +| `oneOf` / `discriminator` | 1 / 0 | | +| `prefixItems` | 6 | | +| `patternProperties` | 2 | | +| empty schemas `{}` | 9 | | + +Component names are widely dotted (`Session.Message.Info`, `Location.Info`) — the existing +mangling rule covers them. Two-day drift against session 12's census of the same branch +(2026-08-11: 322 schemas, 422 `allOf`, 359 single-value enums) measures the churn rate directly +— the branch moves daily, which is exactly what snapshot pinning is for. + +**Streaming reshape.** `text/event-stream` lives on `v2.event.subscribe` and `v2.session.log`; +`v2.pty.connect` carries `x-websocket`; `x-effect-stream` rides both SSE operations +(2026-08-13 ingestion-level correction of this session's earlier jq census). The v1 durable +stream pair (`v2.session.events` + `v2.session.history`, `after`-cursor resume) is replaced by +**`v2.session.log`** (`/api/experimental/session/{sessionID}/log`, params `after` + `follow`) and +cursor pagination on `v2.message.list`. The global `/api/event` stream has **no resume +parameters**. Cursor/after parameters exist on exactly: `session.list`, `session.log`, +`message.list`, `pty.connect`, `shell.output`. The locked SSE-resume design premise must be +re-derived against `session.log` at M3. + +## 6. Server-client architecture: one shared daemon, discovered by file + +The background-service model replaces v1's per-invocation `opencode serve` habit: + +- **Registration file** in the XDG state directory, 0600, content + `{id, version, url, pid, password}`. The code's own words: *"That file is the complete + discovery contract — reading it is all a client needs to connect."* +- **`Service.ensure`** (in `@opencode-ai/client`): reuse a healthy, version-compatible server; + replace a version-mismatched one; otherwise spawn detached, `unref`'d contenders (default + command `opencode serve --service`) — racing clients may spawn several, one wins the + registration, losers exit. The daemon survives its parent. +- **Every client uses the same flow**: the TUI root command (via `ServerConnection.resolve` — + "Starting background server…" / "Restarting background server (version mismatch)…"), the + `api` command, and the desktop app (`background-cli.ts` runs `Service.ensure` with its + bundled CLI; isolated/dev mode redirects `XDG_STATE_HOME` to its own userData for a private + service). +- The server is **project-agnostic**: operations take `location[…]` query addressing + (`LocationQuery`), sessions carry `location.directory`, storage is central — one daemon hosts + every project (hence "session history migration", §1). + +CLI surface (root description: "OpenCode 2.0 preview command line interface"): + +| Command | Behavior | +|---|---| +| `serve` | *"Start the v2 API and web server"* — `--hostname`, `--port`, `--service`, `--stdio` (the last two cannot combine) | +| `service start/stop/restart/status/get/set/unset` | managed background daemon | +| `pair` | prints server URLs + username `opencode` + password + a QR code (uqr) | +| root `--standalone` / `--server ` | private server / explicit remote instead of the daemon | +| `acp` | *"Start an Agent Client Protocol server"* (§7) | +| `api`, `mini`, `run`, `export`, `import`, `mcp`, `models`, `plugin`, `auth`, `console`, `debug` | client-side utilities over the same surface | + +**`serve --stdio`** is lifecycle-over-stdio, not HTTP-over-stdio: a real HTTP server whose +stdout handshake is `JSON.stringify({url})` and whose lifetime is `waitForStdinClose()` — parent +dies, server exits. It scrubs `OPENCODE_PASSWORD`/`OPENCODE_SERVER_PASSWORD` from its +environment and generates a random password. Made for embedding hosts (the desktop uses the +bundled CLI this way). + +**Auth is unchanged from v1**: HTTP Basic, username `opencode`, optional password; service mode +generates `randomBytes(32).toString("base64url")` when unconfigured; foreground `serve` prints +the password when it generated one. The SDK's decoration decisions carry over verbatim; the +`location[…]`-vs-`x-opencode-directory` addressing question is a retarget-time decision. + +## 7. MCP is client-side only; the exposure protocol they chose is ACP + +The `mcp` protocol group (6 ops, including `mcp.resource.catalog`) manages the MCP servers +opencode itself connects to: *"Retrieve configured MCP servers and their connection status"*, +*"Add an MCP server at runtime or replace an existing one, connecting it immediately."* +`packages/core/src/mcp/` is `client.ts` / `stdio.ts` / `oauth.ts` over +`@modelcontextprotocol/sdk` 1.29.0 (patched at the root); the CLI's `mcp` handlers are +add/auth/list/logout/resolve. **No MCP-server exposure exists anywhere on the branch.** The +protocol opencode exposes *itself* over is **ACP**: `opencode acp` — *"Start an Agent Client +Protocol server"* — aimed at editors driving the agent. + +Consequence: the ecosystem gap this repository's MCP-server premise rests on (research docs +03/04) persists on v2 — arguably validated, since upstream sees the expose-the-agent need and +chose the editor-side protocol. ACP is an adjacent surface to watch: it can absorb some +"drive opencode from another agent" use cases, but MCP clients remain unserved. + +## 8. Distribution: scoped npm CLI + a desktop update service, side by side with 1.x + +- **The v2 CLI publishes as `@opencode-ai/cli`** (scoped — not the old `opencode-ai` package): + dist-tag `next` = `0.0.0-next-17403`, registry modified 2026-08-13T05:11Z, build-number + versioning. It installs side by side as **`opencode2`** (`OPENCODE_CLI_NAME` build-time + rename; opencode.ai/v2/docs: "OpenCode 2 installs and runs as opencode2. It does not replace + OpenCode 1's opencode binary"). The old `opencode-ai` package's `next` tag is a June-27 + fossil; both packages share `beta`/`dev`/`latest` tags for the 1.x line — two families, one + registry namespace. +- **The desktop beta rides their own update service**: `update.opencode.ai`, a Cloudflare + Worker (D1 artifact table; OIDC-authenticated `POST /api/publish` against the GitHub Actions + JWKS; channel feeds at `/api/{channel}`). Live probe of `/api/beta`: desktop + `0.0.0-beta-17406`, released 2026-08-13T09:04Z, artifacts on the **separate + `anomalyco/opencode-beta` repository's** GitHub Releases (dmg/deb/rpm/AppImage/exe + + electron-updater manifests). `/api/dev` and `/api/prod`: "Channel not found" — beta is the + live channel. +- Non-prod desktop builds **bundle `opencode-cli*` binaries** (electron-builder + `extraResources`) — desktop-beta users get CLI+server without npm. Channel app IDs: + `ai.opencode.desktop{.dev,.beta,}`. +- The same `opencode-beta` repo also hosts timestamped releases (`v0.0.0-beta-202608110357`, + 37 assets) matching the old npm `beta` dist-tag exactly — the 1.x CLI beta pipeline. Two + pipelines, one artifact repository. +- `publish.yml` maps `ref_name == 'v2'` → channel `next`; `ci: make v2 publishing manual` + landed 2026-08-13 — v2 publishing is currently a manual dispatch. + +## 9. Live verification on this machine (2026-08-13) + +```text +$ npm install -g @opencode-ai/cli@next # postinstall blocked by local allow-scripts +$ opencode2 --version # policy; binary functional regardless +opencode2 v0.0.0-next-17403 +$ opencode2 service status +stopped ← installing starts no server +$ opencode2 serve --hostname 127.0.0.1 --port 41999 +server listening on http://127.0.0.1:41999 +server password <44-char random base64url> +$ curl http://127.0.0.1:41999/api/health → HTTP 401 +$ curl -u opencode: http://127.0.0.1:41999/api/health → HTTP 200 +{"healthy":true,"version":"0.0.0-next-17403","pid":3829434} +$ curl -u opencode: "http://127.0.0.1:41999/api/session?limit=2" +{"data":[{"id":"ses_0706…","title":"…","location":{"directory":"/home/deniz/src/…"}}, …]} +``` + +Three facts proven live: auth is enforced (401 without credentials); the health response is the +reshaped `ServiceHealth` (`{healthy, version, pid}` — richer than v1's bare `{healthy:true}`); +and `session?limit=2` returned this machine's **real pre-existing sessions** in the `{data}` +envelope — the central store (and the v1 data path into it) works. + +## 10. Consequences for this SDK + +The maintainer sealed the retarget (2026-08-13): **single surface, v2 protocol target** — +ADR-0005 revised in place; the M1 Arc B plan gains a retarget task; the legacy hub (old M5) is +never built. + +**Carries over verbatim:** the Basic-auth decoration design; both selected M1 operations +(`v2.session.message` shape-identical with its `{data}` envelope; `v2.health.get` present with a +reshaped response); the discriminator-free `anyOf` union dialect and with it ADR-0009 and the +converter machinery; the launcher premise (M4 maps to `opencode2 serve`; `--stdio` is a +lifecycle-leash bonus; connect-or-launch fits the discovery-file model). + +**Changes at retarget:** the ingestion wall needs one new admit rule (single-element, +validation-only `allOf` unwrap); `HealthResponse` maps `ServiceHealth`; the +`location[…]`-query vs `x-opencode-directory`-header decoration policy must be decided; the M3 +durable-stream design is re-derived against `session.log` (`after` + `follow`). + +## UNVERIFIED / open + +- The beta-desktop→v2-branch CI wiring is not traced line-level; the link is inferred from the + artifact shape (desktop-only), the distinct build-number versioning, same-day cadence with + branch activity, and the branch's electron-builder config. +- v2 GA timing and channel stabilization — no `latest`-equivalent channel exists yet; + `@opencode-ai/cli@next` cadence is unclear now that v2 publishing went manual (2026-08-13). +- `session.log` semantics versus the v1 durable stream (ordering, replay guarantees, the + `follow` flag) — M3 re-research. +- The v2-side error-response shapes of the selected M1 operations — derived by the retarget + task, not yet inspected. +- How long the 1.x line keeps publishing releases alongside v2. diff --git a/docs/superpowers/plans/2026-08-11-m1-walking-skeleton.md b/docs/superpowers/plans/2026-08-11-m1-walking-skeleton.md deleted file mode 100644 index 5245c6cee..000000000 --- a/docs/superpowers/plans/2026-08-11-m1-walking-skeleton.md +++ /dev/null @@ -1,188 +0,0 @@ -# M1 Walking Skeleton Implementation Plan - -Date: 2026-08-11 - -> **For agentic workers:** REQUIRED SUB-SKILL: Use -> `superpowers:subagent-driven-development` (recommended) or -> `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox -> (`- [ ]`) syntax for tracking. - -**Goal:** Land a callable, pre-release SDK surface for `v2.health.get` and -`v2.session.message`, demonstrated once against a real `opencode serve`. - -**Architecture:** `ISpecIngestion` remains the only OpenAPI boundary. A selection-scoped -Binder turns `SpecDocument` plus sparse curation into role-specific EmitPlan records; dumb -Roslyn emitters produce committed source, and generated methods delegate once into a -hand-written HTTP behavior core. Arc A lands the compiler/models; Arc B lands the client. - -**Tech Stack:** .NET SDK 10.0.302, C# 14, Microsoft.OpenApi 3.9.0, -Microsoft.CodeAnalysis.CSharp 5.6.0, System.Text.Json 10.0.11, CliWrap 3.10.4, TUnit, -Verify.TUnit, Testably.Abstractions, and PublicApiGenerator 11.5.4. - -## Global Constraints - -- Binding decisions are `AGENTS.md` plus ADRs 0001-0009; design specs are reference only. -- Emit exactly the two selected modern operations. Pending operations are reported, not - fingerprinted, and keep `dotnet pack` blocked by an owned `.generation-incomplete` marker. -- Clients, options, responses, routes, and exceptions use `OpenCode.Sdk`; generated domain and - error models use `OpenCode.Sdk.Models`; serializer infrastructure is internal. -- The first payload members are `SessionMessageResponse.Message` and - `HealthResponse.Healthy`; health's one-value boolean enum emits as `bool`. -- Every marked union has a position-independent converter and explicit `Unknown*` carrier with - its tag and a cloned raw `JsonElement`; serialization uses only generated metadata. -- Generated output is deterministic, LF-only, manifest-owned, analyzer-visible plain `.cs`. -- Product code targets `netstandard2.0;net472;net8.0;net9.0;net10.0` and uses - `ConfigureAwait(false)`. Tests obey `docs/engineering/testing-style.md`. -- Exclude retry, telemetry, hooks, DI Extensions, launcher, SSE, process harness, coverage/CI - legs, hashes, fingerprints, full curation, and unselected operation support. - -## File Map - -- Inputs: `tools/generation-profile.txt`, `tools/curation.json`. -- Compiler: `tools/OpenCode.Sdk.Tools/Generator/{Binding,Emission,Output}/` and - `Generator/GenerationCoordinator.cs`. -- Product: generated `src/OpenCode.Sdk/{Models,Serialization,Internal}/` plus root clients and - responses; hand-written root spines plus `Internal/Pipeline.cs`. -- Tests: matching `Generator/` areas in Tools.Tests and - `{Serialization,Contract,Support,Fixtures}/` in Sdk.Tests. - ---- - -## Arc A - Selected Compiler And Committed Models - -Use `feature/m1-compiler-models`; each task is one reviewable commit. Merge Arc A before Arc B. - -### Task 1: Selection, Curation, And Binder - -**Files:** Create the two input files; `Binding/Abstractions/ISpecBinder.cs`; -`Binding/{SpecBinder,OperationSelectionLoader,CurationLoader,ReachableSchemaCollector, -CSharpNamePolicy}.cs`; role records under `Binding/Models/`; matching loader/Binder tests. - -**Interface:** `ISpecBinder.Bind(SpecDocument, OperationSelection, GenerationCuration)` returns -`EmitPlan` containing only model, marked-union, registry, and pending-operation plans. - -- [ ] Red-test missing/duplicate IDs, unknown config fields, orphan/pending curation rows, - selected coverage gaps, batched errors, closure, duplicate-ref collapse, and determinism. -- [ ] Check in the exact two-operation profile and sparse curation: root `health`; `Sessions` + - `SessionClient`; payload `Message`; reasoned `Uri` overrides for - `PromptFileAttachment.uri` and `ToolFileContent.uri`. -- [ ] Bind only the selected closure, require every reached union to be marked, apply FDG names - (`Llm*`), and derive pending modern/legacy sets without full-pin count assertions. -- [ ] Run `dotnet test tests/OpenCode.Sdk.Tools.Tests/OpenCode.Sdk.Tools.Tests.csproj - --configuration Release`; commit `feat(generator): bind the M1 operation selection`. - -### Task 2: Model, Union, And Registry Emitters - -**Files:** Run `dotnet add tools/OpenCode.Sdk.Tools/OpenCode.Sdk.Tools.csproj package -Microsoft.CodeAnalysis.CSharp --version 5.6.0`; create -`Emission/{GeneratedSource,SourceEmitter,ModelEmitter,UnionEmitter,RegistryEmitter}.cs` and -matching tests with one Verify micro-snapshot per emitter family. - -**Interface:** `SourceEmitter.Emit(EmitPlan)` returns an ordinal-sorted -`IReadOnlyList`; each item carries a manifest-relative path and UTF-8 source. -Emitters consume no SpecIR, curation, or filesystem. - -- [ ] Red-test immutable records, `required`, empty optional collections, wire names, XML docs, - error inheritance, marked dispatch, unknown round-trip, and registry membership. -- [ ] Emit Roslyn trees for models, `OpenCodeError`, concrete errors, union bases/variants, - `Unknown*` carriers, converters, and internal `OpenCodeJsonContext`. -- [ ] Compile emitted snippets, approve only the three micro-snapshots, run - `dotnet test tests/OpenCode.Sdk.Tools.Tests/OpenCode.Sdk.Tools.Tests.csproj --configuration - Release`, and commit `feat(generator): emit selected models and unions`. - -### Task 3: Writer, Generate Command, And Arc A Gate - -**Files:** Run `dotnet add src/OpenCode.Sdk/OpenCode.Sdk.csproj package System.Text.Json ---version 10.0.11` and `dotnet add tools/OpenCode.Sdk.Tools/OpenCode.Sdk.Tools.csproj package -CliWrap --version 3.10.4`; create `Generator/{GenerationCoordinator,GenerationRequest, -GenerationReport}.cs`, -`Output/Abstractions/{IGenerationWriter,IProjectFormatter}.cs`, and -`Output/{GenerationWriter,CliWrapProjectFormatter,GenerationManifest,WriteResult}.cs`; modify -`GenerateCommand.cs`, `ToolApp.cs`, and `OpenCode.Sdk.csproj`; add product serialization tests -and embedded fixtures; remove `SmokeTests.cs` after real tests cover each test TFM. - -- [ ] Unit-test manifest path safety, stale cleanup, unrelated-file preservation, repeat output, - clean/drift `--verify`, report/marker behavior, and command exits using MockFileSystem and a - substituted formatter. -- [ ] Implement write-then-format ownership. `--verify` snapshots old/new owned paths, writes, - formats the SDK project, byte-compares owned output, and reports changed/created/deleted paths - without Git. -- [ ] Generate and commit `Models/`, internal converters/context, `.generated-manifest.json`, - and `.generation-incomplete`; product-test known/unknown outer, assistant-content, and - tool-state variants, arbitrary marker order, semantic round-trip, and context-only metadata. -- [ ] Run `dotnet run --file tools/opencode-tool.cs -- generate`, repeat with `--verify`, then - run Release build/tests and format. Run - `dotnet pack src/OpenCode.Sdk/OpenCode.Sdk.csproj --configuration Release` separately and - require failure solely from the partial marker. -- [ ] Commit `feat(generator): commit the M1 model closure`; open and merge the Arc A PR. - ---- - -## Arc B - Callable Client - -Use `feature/m1-callable-client` from merged Arc A. - -### Task 4: Hand-Written Behavior Core - -**Files:** Create root `{OpenCodeClientOptions,OpenCodeRequestOptions,ErrorBehavior, -OpenCodeResponse,OpenCodeException,OpenCodeApiException,OpenCodeTransportException}.cs`; -`Internal/Abstractions/IEnvironmentProvider.cs`; -`Internal/{SystemEnvironmentProvider,Pipeline,ResponseAdapter}.cs`; SDK friend assembly metadata; -recording-handler/environment test support and focused runtime tests. - -**Interface:** `Pipeline.ExecuteAsync(HttpMethod, string, -ResponseAdapter, OpenCodeRequestOptions?, CancellationToken)` returns -`Task` where `TResponse : OpenCodeResponse`. Generated adapters own typed success/error -deserialization and envelope construction; Pipeline owns transport and throw/`NoThrow` behavior. - -- [ ] Red-test endpoint joining; explicit password then one-time - `OPENCODE_SERVER_PASSWORD` fallback with Basic user `opencode`; directory precedence; - `x-opencode-directory`; `OpenCode.Sdk/` User-Agent; cancellation; - transport/protocol errors; response/HttpClient disposal. -- [ ] Implement only those behaviors. Preserve `OperationCanceledException`; expose status, - typed `OpenCodeError`, and raw body on `OpenCodeApiException`; never dispose BYO HttpClient. - Options expose `Endpoint`, `Password`, and client `Directory`; request options expose - `ErrorBehavior` (`Default`/`NoThrow`), per-call `Directory`, and static `NoThrow`; - `OpenCodeResponse` exposes `Status`, `IsError`, and `Error`. -- [ ] Run `dotnet test tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj --configuration - Release`; commit - `feat(sdk): add the minimal HTTP behavior core`. - -### Task 5: Client Emission, Contracts, And M1 Closure - -**Files:** Add consumed-only `{EnvelopePlan,ClientPlan,OperationPlan,ErrorMapPlan}` binding records; -create `Emission/{EnvelopeEmitter,RoutesEmitter,ClientEmitter,OperationMethodEmitter, -ResponseAdapterEmitter}.cs`; generate root clients/routes/responses and two internal adapters; -run `dotnet add tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj package PublicApiGenerator ---version 11.5.4`; add `Contract/{HealthContract, -SessionMessageContract}Tests.cs`, API approval, recording support, and embedded payload fixtures. - -**Public surface:** `OpenCodeClient` implements `IDisposable`, has `(Uri)`, -`(Uri, OpenCodeClientOptions)`, `(HttpClient, OpenCodeClientOptions)`, and protected mock -constructors, readonly `Sessions`, and `GetHealthAsync(options, cancellationToken)`; -`SessionsClient.GetSessionClient(sessionId)` returns `SessionClient`, whose -`GetMessageAsync(messageId, options, cancellationToken)` returns `SessionMessageResponse`. - -- [ ] Emit final clients, guarded/escaped IDs and routes, guarded `Healthy`/`Message` getters and - `PrintMembers`, one-expression method delegation, and status maps: health 400/401; message - 400/401/404 with both deduplicated not-found variants and `UnknownOpenCodeError` fallback. -- [ ] Contract-test every selected success/declared error through default and `NoThrow`; test - unknown retention, path escaping, auth/directory/User-Agent, cancellation, transport failure, - and client ownership through the real generated client. -- [ ] Approve the first API baseline; run `generate --verify`, Release build/tests, format, and - Slopwatch. Keep the partial marker and pack refusal. -- [ ] Start `OPENCODE_SERVER_PASSWORD=m1-demo opencode serve --port 4096` in a directory with an - existing message; obtain IDs from `GET /api/session?limit=1` and - `GET /api/session/{sessionID}/message?limit=1`; use a temporary `.scratchpad/` console to print - `Healthy`, concrete message type, and message ID; paste output into the PR. -- [ ] Update `docs/ROADMAP.md`, commit `feat(sdk): generate the first callable client`, and open - the Arc B PR. - -## Final Gates And Stop Conditions - -Run `dotnet build --configuration Release`, `dotnet test --configuration Release --no-build`, -`dotnet format --verify-no-changes --no-restore`, and `dotnet tool run slopwatch analyze ---exclude ".scratchpad/**,external/**" --fail-on warning`. Stop for maintainer review if the -closure reaches a structural union, Binder needs Microsoft.OpenApi/raw JSON, a temporary public -or transport path would later be replaced, output cannot compile uniformly on all five TFMs, -partial breadth can pack, or excluded M1 capabilities become necessary for either call. diff --git a/docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md b/docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md new file mode 100644 index 000000000..243f3227c --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md @@ -0,0 +1,197 @@ +# M1 Arc B Callable Client Execution Plan + +Date: 2026-08-13 + +**Goal:** make `v2.health.get` and `v2.session.message` callable through their final +generated SDK surface, backed by one minimal hand-written HTTP behavior core. + +**Architecture:** SpecIR plus curation binds into client, operation, envelope, route, and +error plans. Generic emitters render those plans without knowing operation IDs, wire group +names, or concrete client names. Generated methods delegate once into a hand-written +`Pipeline`; generated response adapters own operation-specific success and error mapping. + +## Boundaries + +- This is an opencode SDK generator, not a general OpenAPI generator. Arc B supports only + selected opencode dialect shapes consumed by M1 and fails closed on any other selected shape. +- Public API policy is curation-driven. The `session` group row declares `Sessions`, + `SessionClient`, and `sessionID` as its handle parameter. The Binder applies one general + partial-application rule; no Binder or emitter branch names `session` or either selected + operation. All other operations stay pending in M1. +- Envelope payload names derive mechanically from the operation subject (the identifier + segments after the group that do not restate the HTTP method; the group name when none + remain): `Message`, `Health`. `envelopePayloadNames` rows are overrides for collisions + with the response spine and C#-illegal results only — a collision fails generation until + an override lands (maintainer, 2026-08-13; supersedes the earlier required-per-operation + curation). +- C# identifiers use ordinary PascalCase for acronym tokens regardless of length: + `id` -> `Id`, `sessionID` -> `SessionId`, `callID` -> `CallId`, `URL` -> `Url`. + `[JsonPropertyName]` preserves wire spelling; exceptional brand spelling remains curated. +- Optionality and nullability stay independent. An optional non-null collection may be absent + and reads as empty; explicit JSON `null` is rejected. Its public property remains non-null. +- `net472` receives a conditional framework ``; no + `System.Net.Http` NuGet package or second HTTP implementation is added. +- URI constructors own endpoint authority. BYO `HttpClient` requires `options.Endpoint`; the + SDK neither reads nor mutates `HttpClient.BaseAddress`. Endpoints are absolute HTTP(S), carry + no query/fragment, and retain a normalized path prefix. +- Every non-2xx remains an API response. Only status-map-allowed known tags become their concrete + error type; an unknown tag, undeclared status, or known tag at the wrong status becomes + `UnknownOpenCodeError` with its raw payload. Malformed error JSON retains raw body with a null + typed error. Throwing calls carry it on `OpenCodeApiException`; `NoThrow` calls carry it on + `OpenCodeResponse.RawBody`. Unexpected or malformed 2xx responses are protocol failures. + `OperationCanceledException` is never remapped. +- Retry, telemetry, hooks, DI Extensions, launcher, SSE, raw `SendAsync`, bodies, query + parameters, and operation breadth are out of scope. + +## Task 0: Retarget The Spec Pin To The v2 Branch + +**Areas:** `spec/`, `Generator/Ingestion/Walls`, `tools/curation.json`, regenerated models. + +Executes the 2026-08-13 direction (ADR-0005 revised: v2 protocol surface only; platform +evidence: research doc 15). + +- [x] Pin a `v2`-branch snapshot of `packages/protocol/openapi.json` as `spec/openapi.json` + and rewrite `spec/SNAPSHOT.md` provenance (branch, commit, retrieval date, refresh steps). +- [x] Add one ingestion-wall admit rule: a single-element `allOf` wrapper carrying only + validation keywords unwraps into its base schema; every other `allOf` stays refused. + Re-census `prefixItems`/`patternProperties`/empty-schema sites against the new pin. +- [x] Re-derive the selected wire contracts from the new pin: `v2.health.get` success is the + `ServiceHealth` object (`{healthy, version, pid}` observed live 2026-08-13), and + `v2.session.message` keeps the `{data}` envelope over the dotted `Session.Message.Info` + schema. Error maps derived from the v2 responses: health declares 400 `InvalidRequestError` + and 401 `UnauthorizedError`; message adds 404 as an inline `anyOf` of + `SessionNotFoundError` and `MessageNotFoundError`. All four are `_tag`-marked Effect + errors; the pinned operation-plan tests assert the maps. +- [x] Map dotted schema names mechanically (`Session.Message.Info` → `SessionMessageInfo`; + 283 of 324 v2 schemas are dotted, so mechanical is the only sane default). Curated + overrides exist only for collisions, C#-illegal results, and brand spellings — no taste + renames (maintainer, 2026-08-13). +- [x] Adopt the v2 `location[...]` query convention as the only per-request scoping + mechanism (maintainer, 2026-08-13: the v1 ledger is closed — no v1 convention, including + `x-opencode-directory`, survives and no backward compatibility is owed). +- [x] Retire the Modern/Legacy surface split (`SpecSurface`, pending-legacy reporting, + legacy placement rules): the pinned surface is single; simplify selection, curation + validation, and generate reporting accordingly. +- [x] Regenerate the selected model closure; update curation rows, serialization tests, and + fixtures to the v2 shapes; run the full repository gates. + +## Task 1: Model Policy And Operation Binding + +**Areas:** `Generator/Binding/`, `ModelEmitter`, `tools/curation.json`, Binder/emitter tests, +and committed generated models. + +- [x] Change `CSharpNamePolicy` to ordinary PascalCase acronym handling and regenerate the + selected closure. Assert representative `Id`, `SessionId`, `MessageId`, `CallId`, and `Url` + mappings before approving an API baseline. +- [x] Route optional non-null collection copying through an internal helper whose input is + nullable and output is non-null. Preserve immutable recursive copies; verify absent -> empty + and explicit null -> `JsonException` under source-generated System.Text.Json. +- [x] Add `handleParameter` to group curation. Require it and `handleName` together, + validate it names a required path parameter, and never consume a query/body value. Keep + every pending operation flat. +- [x] Add consumed-only `ClientPlan`, `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, + and `ErrorMapPlan` plus an `OperationPlanBinder`. Reuse schema-resolved type names instead of + reinterpreting schemas. +- [x] Bind only ordinary GET + JSON + path-parameter operations with one 200 success and + no body/query/stream/wildcard. Bind the health success object to `HealthResponse.Health`, + the `{data}` envelope to `SessionMessageResponse.Message`, and the error maps Task 0 derived. +- [x] Test complete plans, handle-versus-method parameters, collisions, documentation, status + maps, batched failures, and determinism. A synthetic same-shape operation with different names + must bind without new production branches; an operation in a group without a handle + declaration must remain flat even when it carries `sessionID`. + +## Task 2: Hand-Written HTTP Core + +**Areas:** `src/OpenCode.Sdk/OpenCode.Sdk.csproj`, root options/response/exception types, +`Internal/Abstractions/IEnvironmentProvider`, +`Internal/{Pipeline,ResponseAdapter,SystemEnvironmentProvider}`, friend metadata, and tests. + +- [x] Add the conditional `net472` framework reference. Add `ErrorBehavior`, client/request + options, `OpenCodeResponse` including nullable error-path `RawBody`, and the + `OpenCodeException` spine with complete XML documentation. +- [x] Implement endpoint validation/joining and owned-versus-injected `HttpClient` lifetime. + Disposing the root makes its wrappers unusable and never disposes a BYO client. +- [x] Resolve explicit password before one-time `OPENCODE_SERVER_PASSWORD` fallback. Decorate + each request, never default headers, with Basic user `opencode` and the + `OpenCode.Sdk/` User-Agent; per-request scoping rides the v2 + `location[...]` query convention, bound as operation parameters rather than a header. +- [x] Use only `HttpClient.SendAsync(request, ResponseHeadersRead, cancellationToken)`. Own each + request and buffered response, preserve cancellation, and wrap network/protocol failures as + `OpenCodeTransportException`. +- [x] Centralize default throw versus per-call `NoThrow` in + `Pipeline.ExecuteAsync(HttpMethod, string, ResponseAdapter, + OpenCodeRequestOptions?, CancellationToken)` where `TResponse : OpenCodeResponse`. Adapters + deserialize only through `OpenCodeJsonContext` and construct success/error envelopes. + +## Task 3: Generated Callable Surface And Ownership + +**Areas:** new emitters, `SourceEmitter`, `GenerationWriter`, analyzer config, CI, and tests. + +- [x] Emit one file per public type: `OpenCodeClient`, `SessionsClient`, `SessionClient`, both + responses, and `OpenCodeRoutes`; emit adapters under `Internal/ResponseAdapters/`. + `OperationMethodEmitter` returns members to `ClientEmitter`, never partial operation files. +- [x] Emit virtual client members/protected mock constructors, immutable handle state, escaped + routes, guarded payload getters and `PrintMembers`, declared exception XML, and one pipeline + delegation per operation method. +- [x] Extend ownership to root generated files and the adapter subtree. Refuse unmanifested + overwrites and refuse overwrite/deletion of manifest entries without the exact provenance + header; retain case-insensitive collision checks and deterministic stale cleanup. +- [x] Add one micro-snapshot per emitter family, aggregate generated-source compilation, and + writer tests for mixed-root preservation, stale deletion, provenance, formatting, and drift. +- [x] Enable the decided CS1591 gate, document hand-written public members, add only necessary + rule arbitration, and add Linux CI `generate --verify`. + +## Task 4: Contracts And M1 Closure + +- [x] Test real generated clients through a recording `HttpMessageHandler`: health 200/400/401; + message 200/400/401 and both 404 variants; default and `NoThrow`; unknown/malformed errors; + guarded payloads; route escaping; endpoint joining; auth/directory/User-Agent; cancellation; + transport failure; response disposal; owned/BYO lifetime; and protected mock seams. +- [x] Assert an undeclared status and a known tag returned at the wrong declared status both + produce `UnknownOpenCodeError` with the exact tag and raw payload. +- [x] For malformed non-2xx JSON, assert `Error` is null and exact `RawBody` is retained on both + `OpenCodeApiException` and the `NoThrow` response. Assert success responses have no raw body. +- [x] Pin `PublicApiGenerator` 11.5.4 centrally, add it plus `Verify.TUnit` to + `OpenCode.Sdk.Tests`, and review one baseline after casing and callable surface settle. Keep + `.generation-incomplete`; packing must still fail only because breadth is pending. +- [x] Run `generate`, clean `generate --verify`, Release build, supported test legs, format, + Slopwatch, and intentional pack refusal. +- [x] Start a password-enabled real `opencode2 serve` (v2 pre-release channel; record the + exact installed build version as demo evidence). Obtain IDs with authenticated raw calls to + the pinned surface's session-list and message-list operations, then call only the two + generated operations from a temporary `.scratchpad/` console. Print the health payload, + concrete message type, and message `Id`; paste command/output into the PR and move + `docs/ROADMAP.md` to M2. + +## Acceptance Shape + +```csharp +using var client = new OpenCodeClient(new Uri("http://localhost:4096")); + +HealthResponse health = await client.GetHealthAsync(cancellationToken: cancellationToken); +SessionClient session = client.Sessions.GetSessionClient(sessionId); +SessionMessageResponse message = await session.GetMessageAsync( + messageId, + options: requestOptions, + cancellationToken: cancellationToken); +``` + +Stop for maintainer review if binding needs Microsoft.OpenApi/raw JSON, an emitter needs an +operation/client-specific branch, the selected shape exceeds M1 capabilities, a structural +union appears, ownership cannot distinguish hand-written files, a temporary public/transport +API becomes necessary, or any locked product TFM cannot compile. + +Run these final repository gates: + +```text +dotnet run --file tools/opencode-tool.cs -- generate +dotnet run --file tools/opencode-tool.cs -- generate --verify +dotnet build --configuration Release +dotnet test --configuration Release --no-build +dotnet format --verify-no-changes --no-restore +dotnet tool run slopwatch analyze --exclude ".scratchpad/**,external/**" --fail-on warning +dotnet pack src/OpenCode.Sdk/OpenCode.Sdk.csproj --configuration Release +``` + +The first six commands must succeed. The pack command must fail only because +`.generation-incomplete` records pending operations. diff --git a/docs/superpowers/specs/2026-08-09-generator-architecture.md b/docs/superpowers/specs/2026-08-09-generator-architecture.md index c3456a9df..67fcb3e19 100644 --- a/docs/superpowers/specs/2026-08-09-generator-architecture.md +++ b/docs/superpowers/specs/2026-08-09-generator-architecture.md @@ -1,10 +1,14 @@ # Generator Architecture — the model-layer generator and repo tooling -Date: 2026-08-11 +Date: 2026-08-13 > **Status: vision / reference — not sealed.** Binding decisions live in the ADRs and > `AGENTS.md`; this document is direction and design rationale, not law. Contradicting it > is a finding to note, not a deviation-protocol event. +> +> The v1.x dual-surface facts here (operation counts, the legacy hub, envelope inventories) +> predate the 2026-08-13 v2 retarget (ADR-0005) and are historical; the mechanisms remain +> reference material. Design specification produced by the generator-architecture brainstorm session (2026-08-09, ROADMAP queue item 1). Every decision below was discussed and sealed individually with the @@ -132,18 +136,10 @@ System.Text.Json for curation parsing, Microsoft.CodeAnalysis.CSharp for emissio - **`generate`** — parse → bind → emit → write. Fingerprint verification (§9) runs inside every invocation, not as a separate command. - - `--verify` — CI/regen-verify mode: full in-place regeneration (including the format - post-step), then `git status --porcelain` over the generated paths; dirty ⇒ nonzero exit - plus the file list. Precondition, self-checked: `--verify` refuses to start when the - generated paths already carry uncommitted changes, exiting with a distinct - "dirty generated paths" error — a pre-dirty tree would otherwise surface as false - drift (CI checkouts are clean; the check exists for local runs; hand-written WIP - outside the generated paths does not block it). In-memory comparison was considered - and rejected: the `dotnet format` post-step requires an MSBuild workspace, so a - memory-side compare would diff unformatted output against formatted commits and always - report drift. In-place regen + git-clean check is the ecosystem's regen-verify shape - (upstream commits and regen-verifies its generated client the same way — research - doc 08). + - `--verify` — CI/regen-verify mode: snapshot old/new manifest-owned paths, regenerate and + format in place, byte-compare the final owned artifacts, and return nonzero with the + created/changed/deleted path list when drift existed. It repairs drift as part of the run + and has no Git or clean-worktree precondition. - `--update-fingerprints` — the explicit human review gate for fingerprint drift (§9). - **`refresh-spec --ref `** — the spec-refresh workflow (§10). @@ -221,58 +217,19 @@ refuses with zero diagnostics (run-proven: raw `prefixItems`, injected `allOf`/`if`/type-arrays/`discriminator`, typed `headers`/`callbacks`/`webhooks`/ `$defs` payloads all load clean) — hence the projection wall below. -**Projection.** One DOM visitor walks the document once and produces the SpecIR plus -batched, located errors (§2 principle 5; JSON-pointer-style locations). Everything -opencode-specific lives here: - -- **Dialect wall — whitelist-shaped, per host type.** The wall covers **every - consumed DOM type**, not only schemas: the library types constructs silently at - every level (path-level `parameters` would otherwise drop a real parameter; - `headers`, `callbacks`, `webhooks`, media `ItemSchema`, content-based parameters - and non-deepObject styles all land typed with zero diagnostics **[verified]**). - Each host carries a recorded admitted / known-ignored / refused member table; any - populated *spec-derived* member outside its table refuses (library bookkeeping - members — `BaseUri`, `Self`, `Workspace`, `Metadata` — sit outside the wall but - inside the tripwire snapshots): - - | Host | Admitted | Known-ignored | Refused explicitly | - |---|---|---|---| - | document | `paths`, `components.schemas` (+ the version gate above) | `info`, `security`, `tags` | `webhooks`, `servers`, `jsonSchemaDialect`, other `components` members | - | path item | the five HTTP methods | — | path-level `parameters`, `$ref`, `servers`, other methods | - | operation | `operationId`, `summary`, `description`, `deprecated`, `parameters`, `requestBody`, `responses` | `tags`, `security` | `callbacks`, `servers` | - | parameter | `name`, `in` (`path`/`query`), `schema`, `required`, `style`+`explode` only as the `deepObject`+`true` pair | — | `content` (content-based parameters), other styles and locations, `examples` | - | request body | `content` (exactly one media entry), `required` | — | everything else | - | response | `description`, `content` (0 or 1 media entries) | — | `headers`, `links` | - | media type | `schema`; `x-effect-stream` on `text/event-stream` only | — | `itemSchema`/`itemEncoding`/`prefixEncoding` (3.2 members already typed in the pinned DOM), `encoding`, `examples` | - | schema | the node-kind constraint members (list below); `description` and `format` (both recorded on the node) | `pattern`, `minimum`, `maximum`, `exclusiveMinimum`, `minItems`, `maxItems` (validation-only — the pin's population) | `allOf`, type arrays, `discriminator`, `not`, `if`/`then`/`else`, `dependentSchemas`/`dependentRequired`, `propertyNames`, `contains`, `unevaluatedProperties`/`unevaluatedItems`, `$defs` and dynamic anchors, `title`, `default`, `examples`, `readOnly`/`writeOnly`, `xml`, `externalDocs`, `minLength`/`maxLength`, `multipleOf`, `minProperties`/`maxProperties`, `uniqueItems` | - - `UnrecognizedKeywords` must be empty at every schema except the admitted - raw-keyword sites (today exactly one: `prefixItems` under `Config.plugin`; the - admit rule names its site, so a moved or multiplied site refuses loudly — the - active `v2`-branch spec carries 6 `prefixItems` sites at other locations, every - one reported **[verified]**). Unresolved reference targets refuse; `$ref` siblings - beyond `description`/`summary` refuse (reference members proxy to the target - *except* sibling annotations — the projection reads references explicitly, never - through the proxy **[verified]**). Sibling detection rides the raw key-scan - ingestion already performs for the raw-content hashes: the typed members cannot - distinguish a local sibling from the proxied target value **[verified]**. Extension dispositions per host: - `x-codeSamples` (operations) known-ignored, `x-websocket` (operations) recorded as - a flag, `x-effect-stream` (SSE media only) carried opaque, any other `x-*` at any - host refuses. The wall is the drift radar in action: run against the active - upstream `v2`-branch spec it reports that document's 422 `allOf` sites and 6 - relocated `prefixItems` sites individually, batched and located **[verified]** — - upstream evolution arrives as a reviewed admit-rule, never as silent - mis-generation. -- **Library-upgrade tripwires.** A version bump must not move the wall silently: one - test pins that `prefixItems` still lands in `UnrecognizedKeywords`; a reflection - snapshot covers the member inventory **and fresh-instance default values** of - every consumed DOM type (defaults carry wall semantics: `UnevaluatedProperties` - and `AdditionalPropertiesAllowed` default `true` **[verified]**); and a serialized - SpecIR-of-the-pin Verify snapshot catches behavior drift the member lists cannot - see (iteration order, reference resolution, diagnostics) — it changes only on a - spec refresh or an admit-rule change, both reviewed events, and doubles as the - §10 refresh-diff serialization. The admitted-member tables are recorded - decisions, never inferred from the library. +**Projection.** Explicit projectors produce minimal SpecIR plus batched, located errors. +Validation is semantic-risk fail-closed: a selected construct refuses when dropping it could +change emitted wire or public behavior, while descriptive or unconsumed library metadata does +not create a maintenance wall. Standing guards cover reader version, diagnostics, exception +translation, unsupported selected operation/media shapes, unresolved references, `$ref` +siblings, the pinned raw `prefixItems` adapter, and no Microsoft.OpenApi leakage beyond +ingestion. Generated-source diffs plus focused semantic/runtime tests are the primary drift +radar; there is no reflection member/default inventory or full SpecIR snapshot. + +Extension handling is explicit: `x-codeSamples` is known-ignored, `x-websocket` projects +operation behavior, and `x-effect-stream` is carried on SSE media. Other extensions are located +informational drift unless a selected projection would otherwise lose behavior it claims to +emit. - **Unrestricted schemas.** A schema with **no admitted constraint member** populated (annotations such as `description` permitted) accepts any JSON value and projects to an explicit any-value node — 19 sites in the pin, including @@ -318,14 +275,11 @@ never a document-global counter) — classified into the semantic node kinds the Binder consumes: object (including the six hybrid objects carrying both properties and an `additionalProperties` schema), dictionary, free-form, **unrestricted**, union (marked/structural), enum, literal, special-number, tuple, -content-encoded-string, nullable, primitive, ref. Nodes carry `description` and -`format` (Binder doc-text and payload-rule inputs). Ingestion additionally computes -a **raw-content SHA-256 per operation and per named schema** (canonical JSON over -the raw `JsonNode` subtree) and carries them as opaque strings — the §9 fingerprint -source; the Binder composes and persists, and never re-reads the spec. Ordering: + content-encoded-string, nullable, primitive, ref. Nodes carry `description` and + `format` (Binder doc-text and payload-rule inputs). Hashes are not carried before the + excluded/hand-wired fingerprint feature has a consumer. Ordering: schema-graph keys sort ordinal; operation lists and object member order are -document order — DOM iteration order is *observed* to be document order, not a -library contract, so the SpecIR-of-the-pin snapshot guards it across version bumps. + document order — generated-source determinism tests guard the ordering that reaches output. The exact record inventory is derived backward from Binder, emitter and refresh-diff consumption at slice planning — a field nothing consumes is not carried. @@ -362,15 +316,15 @@ The Binder is where every decision lands. Inputs: SpecIR + `curation.json`. Step a row referencing an unreachable schema is an error. Upstream corroborates the dead-schema stance: its own SDK build deletes unreachable `SessionNext*1` schemas from the document before generation (`packages/sdk/js/script/build.ts`). -4. **Name computation** — mechanical PascalCase with `[JsonPropertyName]` wire fidelity; - FDG acronym casing with curated brand exceptions (ADR-0004); dotted-schema-name mangling +4. **Name computation** — mechanical ordinary PascalCase, including acronym tokens, with + `[JsonPropertyName]` wire fidelity and curated brand exceptions (ADR-0004); dotted-schema-name mangling (7 dotted names: `session.status`, `question.replied`, `question.rejected`, `Event.tui.*` ×4 **[verified]**; doc 09's requirement); trailing-digit names (`ProviderAuthError1`, `UnknownError1` **[verified]**; the third such name, `OutputFormat1`, sits outside the reachable closure and is never emitted) pass through - mechanically unless a curated schema-name override renames them; the bound-handle rule - (operations with a `{sessionID}` path parameter emit into `SessionClient` — ADR-0008) - plus curated handle children. + mechanically unless a curated schema-name override renames them; curation declares each + bound handle's collection name, handle name, and required path parameter, and the Binder + partially applies that parameter without operation-specific branches (ADR-0008). 5. **Derived emission decisions** — which operations get forward-only paginators (those with the `{cursor, data}` envelope), which unions get tolerant converters (all *marked* unions — ADR-0009's rule is mechanical over them; the structural-union emission @@ -383,9 +337,8 @@ The Binder is where every decision lands. Inputs: SpecIR + `curation.json`. Step models: the pinned spec documents 185/188 operations but only 3/472 schemas and 27/1836 properties **[verified]** — CS1591 stays `error` with no exemption, so emission must cover every public member. -6. **Fingerprint computation** for every excluded or hand-wired operation (§9), composed - from the raw-content hashes ingestion carries on SpecIR (§4.1) — the Binder never - re-reads the spec. +6. **Fingerprint computation** is deferred until the first excluded or hand-wired operation + enters the intended product surface (§9); no unused hash facts live in SpecIR beforehand. Output: **EmitPlan** — the complete file list with final names, namespaces, members, plus registry, routes, paginator, and manifest contents. Emitters receive no open questions. @@ -415,7 +368,7 @@ is a few hundred rows, which justifies the dedicated data file. | Field | Content | Example | |---|---|---| -| `groups` | wire group → client name; optional handle `{name, children}` | `session` → `Sessions`, handle `SessionClient`, children `[Permissions, Questions, Revert, Events]` | +| `groups` | modern wire group → placement and client name; paired optional `handleName` + required path `handleParameter` | `session` → client `Sessions`, handle `SessionClient`, parameter `sessionID` | | `envelopePayloadNames` | opId → payload property name | `v2.session.list` → `Sessions` | | `exclusions` | `[{op, reason}]`, reason mandatory | `pty.connect` (both surfaces): WebSocket upgrade masquerading as GET (doc 10; public API spec §14) | | `contentTypePayloads` | parameter-stripped media type → `stream` / `string` (§4.1 normalization) | `application/octet-stream` → `stream`; `text/x-diff` → `string` (matches the wire's `text/x-diff; charset=utf-8`) **[verified: the only two non-JSON, non-SSE response content types]** | @@ -426,15 +379,18 @@ is a few hundred rows, which justifies the dedicated data file. ### 5.3 Fail-closed mechanics — the drift radar, all four layers -**Layer 1 — Binder coverage checks (bidirectional set comparison, batched):** +**Layer 1 — Binder coverage checks (bidirectional set comparison, batched):** during staged +generation, spec-to-curation checks cover only selected operations and their reachable closure; +curation-to-spec checks remain global so orphan rows always fail. The release profile expands +the forward checks to both complete surfaces. Legacy placement remains mechanically flat and +does not require modern group-handle rows. | Direction | Check | Failure message shape | |---|---|---| -| spec → curation | every operationId ∈ naming-map coverage ∪ exclusions | `operation 'v2.widget.list': no curation entry` | -| spec → curation | every operationId-prefix group has a `groups` row | `group 'widget': unnamed` | -| spec → curation | every enveloped response has a payload name | `v2.widget.list: payload unnamed` | -| spec → curation | every response content type ∈ map (JSON and SSE built in) | `image/png: unmapped content type` | -| spec → curation | every `anyOf`-null field has a null-semantics decision (7 model fields **[verified]** — an eighth `anyOf`-null location sits inside the opaque `x-effect-stream` metadata and never reaches the schema graph, §4.1) | ADR-0004: an unmapped `anyOf`-null fails generation | +| selected spec → curation | every selected modern group has a `groups` row | `group 'widget': unnamed` | +| selected spec → curation | every selected enveloped response has a payload name | `v2.widget.list: payload unnamed` | +| selected spec → curation | every selected response content type is supported (JSON and SSE built in) | `image/png: unmapped content type` | +| selected closure → curation | every reached `anyOf`-null field has a null-semantics decision | ADR-0004: an unmapped `anyOf`-null fails generation | | curation → spec | every curation key references an existing spec construct | `curation row 'session.prompt': matches nothing` (renames orphan their rows — orphans are errors, or the config rots silently) | Upstream comparison: `httpapi-codegen` throws `GenerationError` on every ambiguity it meets @@ -520,31 +476,33 @@ folder convention. 1. **Output manifest** — `src/OpenCode.Sdk/.generated-manifest.json`, the sorted list of generated file paths, committed. Files present in the previous manifest but absent from the current plan are deleted (no zombie API); the Writer never **writes or deletes** - outside the manifest (hand-written code is structurally safe from emission and cleanup — - only the project-wide format post-step, below, may touch it). Pattern precedent: + outside the manifest. Mixed generated/hand-written roots additionally refuse unmanifested + overwrites and require the exact provenance header before overwrite or deletion. Pattern precedent: upstream's `.httpapi-codegen.json` manifest with stale-file removal and unsafe-path refusal (`index.ts`, `write()`). The test-consumed artifacts (operation inventory and contract fixtures — testing spec §7.1/§3) form a second manifest root under the same write/delete discipline. 2. **Determinism** (§2 principle 4): stable orderings, culture-invariant formatting, LF endings — byte-identical output for identical inputs. -3. **`dotnet format` post-step** runs on the whole SDK project rather than a per-file - include list: hundreds of `--include` paths would strain the Windows command-line length - limit, whole-project formatting is idempotent, and it matches what the CI format gate - checks anyway. Consequence, stated openly: running `generate` with unformatted - hand-written WIP in the project will format that WIP too — the post-step is project-wide - by design and applies only the rules the CI gate enforces anyway. Upstream's equivalent - is prettier-per-file inside `write()`. +3. **`dotnet format` post-step** receives only the current manifest-owned source paths through + `--include`, so generation cannot rewrite hand-written WIP. `--verify` snapshots old/new + owned paths, regenerates and formats them, then byte-compares without depending on Git. + Upstream's equivalent is prettier-per-file inside `write()`. ## 9. Fingerprint manifest (ADR-0008 mechanics) +This section is a deferred design. It has no Arc B consumer and does not add fields to current +SpecIR. Before the first excluded or hand-wired operation enters the intended full surface, the +implementation plan must choose and test a canonical subtree acquisition mechanism without +making Binder or emitters parse OpenAPI. + - **Location:** `spec/fingerprints.json` — next to the spec pin, because what it pins are spec subtrees. Committed; written only by the tool. - **Scope:** every operation that bypasses generation — the curated exclusions (`v2.pty.connect`, legacy `pty.connect` — public API spec §14) and the four hand-wired SSE stream endpoints (§4.1 list). Entries carry `{surface, kind, hash, reason}`; `kind` (`excluded` | `handwired`) selects the hash coverage. -- **Hash:** SHA-256 composed from the raw-content hashes ingestion computes (§4.1: +- **Hash:** SHA-256 over canonical raw subtrees acquired by the fingerprint feature: canonical JSON per raw subtree — sorted keys, no whitespace, so cosmetic reordering of the spec file cannot move a hash); the composition depends on `kind`: - `excluded` — the full subtree: the **HTTP method and path** (included explicitly — the @@ -630,7 +588,7 @@ follows is the sealed tooling-test design. compile harness dissolves into the product build itself: committed output compiles in normal CI (5 TFMs × 3 OSes × the on-merit analyzer wall). - **Inventory & fixtures:** inventory fidelity to curation (excluded ops absent, SSE ops - flagged); fixture-synthesis determinism, snapshot-verified. + flagged); fixture-synthesis determinism by direct byte comparison. - **Writer/commands:** MockFileSystem for manifest write and stale deletion; git/format calls behind `Infrastructure` wrapper interfaces, faked in tests; command wiring via `CommandAppTester`; `refresh-spec` covered the same way (faked git/copy wrappers, @@ -694,17 +652,15 @@ build itself is the compile gate for generated output (§11). (§5.3; exceeds upstream's one-directional `omitEndpoints`). 5. **Pipeline is two-stage** — Microsoft.OpenApi reader → projection → SpecIR → Binder → EmitPlan → Emitters → Writer (§4; ingestion: ADR-0003). Ingestion - mechanics sealed with it: per-host wall tables, the `OpenApi3_1` version gate, - reader diagnostics as errors, the DOM-boundary guard tests, envelope-root - subtraction, and ingestion-computed raw-content hashes as the fingerprint source - (§4.1, §4.2, §9, §11). + mechanics retain the `OpenApi3_1` version gate, reader diagnostics as errors, targeted + semantic guards, DOM-boundary tests, and envelope-root subtraction (§4.1, §4.2, §11). 6. **File mechanics:** plain `.cs` names, non-magic do-not-edit header, no `[GeneratedCode]`, no per-file `#nullable`; manifest tracks generated-ness (§7). -7. **Writer:** output manifest + stale cleanup; whole-project `dotnet format`; - `--verify` = in-place regen + git-porcelain check (in-memory compare rejected — §3.2/§8). -8. **Fingerprints:** `spec/fingerprints.json`, canonical-JSON SHA-256 with two-kind - coverage — full subtree (method and path included) for exclusions, transport shape - for the hand-wired SSE ops — `--update-fingerprints` as the human review gate (§9). +7. **Writer:** output manifest + stale cleanup; owned-path `dotnet format`; `--verify` = + in-place regen plus before/after byte comparison without Git (§3.2/§8). +8. **Fingerprints are deferred until consumed:** the first excluded or hand-wired operation + lands `spec/fingerprints.json`, canonical subtree hashing, two-kind coverage, and the human + update gate together (§9). 9. **Refresh procedure is a playbook, not an ADR** — canonical home `spec/SNAPSHOT.md`, rewritten to the tool-based flow when the tool lands (§10). 10. **Multi-TFM: uniform source, zero `#if`;** inexpressible constructs fail generation diff --git a/docs/superpowers/specs/2026-08-09-public-api-design.md b/docs/superpowers/specs/2026-08-09-public-api-design.md index e443e5352..044f83718 100644 --- a/docs/superpowers/specs/2026-08-09-public-api-design.md +++ b/docs/superpowers/specs/2026-08-09-public-api-design.md @@ -1,10 +1,14 @@ # Public API Design — OpenCode.Sdk & OpenCode.Sdk.Extensions -Date: 2026-08-11 +Date: 2026-08-13 > **Status: vision / reference — not sealed.** Binding decisions live in the ADRs and > `AGENTS.md`; this document is direction and design rationale, not law. Contradicting it > is a finding to note, not a deviation-protocol event. +> +> The v1.x dual-surface facts here (operation counts, the legacy hub, envelope inventories) +> predate the 2026-08-13 v2 retarget (ADR-0005) and are historical; the mechanisms remain +> reference material. Design specification produced by the public-API brainstorm session (2026-08-08/09, ROADMAP queue item 1). Every decision below was discussed and sealed individually with the @@ -96,7 +100,8 @@ ADR-0006 and unchanged. This spec adds/confirms: ### 4.1 Decision -All failures surface through a **typed exception spine**: +API failures throw through a **typed exception spine** by default; per-call `NoThrow` returns +them on the response spine. Transport and protocol failures always throw: ```csharp public class OpenCodeException : Exception { } // base — one catch point @@ -205,6 +210,7 @@ public abstract record OpenCodeResponse public required int Status { get; init; } public bool IsError { get; init; } public OpenCodeError? Error { get; init; } // populated only on NoThrow error path + public string? RawBody { get; init; } // populated only on NoThrow error path } public sealed record SessionListResponse : OpenCodeResponse @@ -230,8 +236,8 @@ public sealed record SessionListResponse : OpenCodeResponse map to named properties (`Location`, `Cursor` alongside the payload). A handful of modern responses are not enveloped (`{healthy}`, bare `LocationInfo`, `ProjectCopyCopy`) — the fail-closed curation map names their payloads like any other. -- 204 operations return an envelope with no payload property (`Status`/`IsError`/`Error` - only). +- 204 operations return an envelope with no payload property beyond the shared response + metadata. - Payload property names come from the curation map (fail-closed: an unmapped envelope breaks generation, § 8.5). - Pagination follows upstream's Page discipline (doc 02): continuation accepts **only the @@ -447,12 +453,18 @@ broken build. Method bodies are one-line delegations — ```csharp public virtual Task GetAsync( string sessionId, OpenCodeRequestOptions? options = null, CancellationToken cancellationToken = default) - => Pipeline.ExecuteAsync(OpenCodeRoutes.Sessions.Get(sessionId), options, cancellationToken); + => Pipeline.ExecuteAsync( + HttpMethod.Get, + OpenCodeRoutes.Sessions.Get(sessionId), + SessionGetResponseAdapter.Instance, + options, + cancellationToken); ``` — behavior (NoThrow, retry, error mapping) lives once in the hand-written core (§9), never -in bodies. The bound-handle projection is one mechanical generator rule (ops with a -`{sessionID}` path parameter emit into `SessionClient`). Hand-written remains the identity +in bodies. Modern group curation may declare a bound handle's collection name, handle name, and +required path parameter; the Binder mechanically partially applies that parameter. Legacy stays +flat. Hand-written remains the identity core: transport pipeline, SSE engine + stream endpoint wiring (stream endpoints are detected by their `text/event-stream` content type — `x-effect-stream` exists only on the durable endpoint — and are hand-wired; the generator emits their item schemas), @@ -627,9 +639,9 @@ forward-compatibility question parked by the codegen spike. generated `*Url` string property breaks the build and forces Uri-or-arbitration. Escape hatch if version skew ever delivers malformed URLs: per-property fallback to string, recorded in curation. -2. **Acronym casing: Framework Design Guidelines** — 3+ letters PascalCase (`ApiError`, - `Pty`, `Mcp`, `Tui`, `Vcs`, `Lsp`), two letters upper, brand spellings via curated - exceptions (`OAuth`). Resolves the spike's S101/CA1707 findings mechanically. +2. **Acronym casing: ordinary PascalCase** — all acronym tokens normalize consistently + (`Id`, `Io`, `Ip`, `Url`, `Api`, `Pty`, `Mcp`, `Tui`, `Vcs`, `Lsp`); brand spellings use + curated exceptions (`OAuth`). Resolves the spike's S101/CA1707 findings mechanically. 3. **Identifier mapping** — every wire name maps mechanically to PascalCase with `[JsonPropertyName]` carrying wire fidelity (`_tag`, snake_case, dotted schema names per doc 09's mangling requirement). diff --git a/docs/superpowers/specs/2026-08-10-testing-architecture-design.md b/docs/superpowers/specs/2026-08-10-testing-architecture-design.md index 1d81937e9..235bdcb80 100644 --- a/docs/superpowers/specs/2026-08-10-testing-architecture-design.md +++ b/docs/superpowers/specs/2026-08-10-testing-architecture-design.md @@ -1,10 +1,14 @@ # Testing Architecture & Strategy — OpenCode.Sdk, OpenCode.Sdk.Extensions, tools/ -Date: 2026-08-11 +Date: 2026-08-13 > **Status: vision / reference — not sealed.** Binding decisions live in the ADRs and > `AGENTS.md`; this document is direction and design rationale, not law. Contradicting it > is a finding to note, not a deviation-protocol event. +> +> The v1.x dual-surface facts here (operation counts, the legacy hub, envelope inventories) +> predate the 2026-08-13 v2 retarget (ADR-0005) and are historical; the mechanisms remain +> reference material. Design specification produced by the testing-architecture brainstorm session (2026-08-09/10, ROADMAP queue item 1 — its final design step). Every decision below was discussed and sealed @@ -483,7 +487,8 @@ this session's to revisit). Revisions: 1. **New tool outputs get tests.** This design added two tool responsibilities (operation inventory §7.1, contract fixtures §3 level 2): Tools.Tests covers inventory fidelity to - curation (exclusions absent, SSE flagged) and fixture-synthesis determinism (snapshots). + curation (exclusions absent, SSE flagged) and fixture-synthesis determinism by direct byte + comparison. 2. **`refresh-spec` command tests** were missing from the sketch: faked git/copy wrappers, `SNAPSHOT.md` rewrite, diff-summary output — `CommandAppTester` + MockFileSystem. 3. **Placement clarification:** §11's "round-trip behavior" tests (known tag → variant, @@ -536,9 +541,8 @@ format gate, and the generator spec §13 `generate --verify` step): ## 12. Fixtures, snapshots, coverage philosophy -- **Verify has exactly three uses** (sealed): emitter micro-snapshots (§10), the ingestion - SpecIR-of-the-pin snapshot (generator spec §4.1's library-upgrade tripwire), and the - **public API surface lock** — a PublicApiGenerator-style surface dump per package under Verify approval, +- **Verify has exactly two uses**: emitter micro-snapshots (§10) and the **public API surface + lock** — a PublicApiGenerator-style surface dump per package under Verify approval, which turns any member removal or signature change into a reviewable diff (the ROADMAP queue-1 `api-design`/`snapshot-testing` intent made concrete). Behavior tests never use snapshots — assertion intent stays explicit in the test body. @@ -577,7 +581,6 @@ format gate, and the generator spec §13 `generate --verify` step): installer script, version pinning syntax) — resolved when the CI legs are built. - **Health endpoint choice** for container readiness probing (modern `global.health` vs a cheaper TCP-only check). -- **Verify.TUnit** package addition to `Directory.Packages.props`. - **Fake-binary launcher test technique** portability (script vs tiny compiled helper) across the three OS legs. Reference for its scripted stdout: upstream's own readiness parse is `/listening on (http:\/\/([^\s:]+):(\d+))/` (`cli-process.ts`) — the fake binary replays diff --git a/docs/superpowers/specs/2026-08-11-production-walking-skeleton-design.md b/docs/superpowers/specs/2026-08-11-production-walking-skeleton-design.md index 64b562f04..a858d4d7b 100644 --- a/docs/superpowers/specs/2026-08-11-production-walking-skeleton-design.md +++ b/docs/superpowers/specs/2026-08-11-production-walking-skeleton-design.md @@ -1,6 +1,6 @@ # Production walking skeleton: first callable SDK milestone -Date: 2026-08-11 +Date: 2026-08-13 > **Status: vision / reference — not sealed.** Binding decisions live in the ADRs and > `AGENTS.md`; this document is direction and design rationale, not law. Contradicting it @@ -346,9 +346,9 @@ method bodies. ## Public Surface -The selected modern health group is a single-operation root group, so it produces a root client -method. The selected message operation carries `{sessionID}` and therefore lives only on the -bound session handle. The intended shape is: +The selected modern health group is curated as a single-operation root group, so it produces a +root client method. The session group curation declares `{sessionID}` as its handle parameter, +so the selected message operation lives only on the bound session handle. The intended shape is: ```csharp var client = new OpenCodeClient(new Uri("http://localhost:4096")); @@ -387,7 +387,7 @@ The runtime arc lands only public members whose behavior is complete: | `OpenCodeRequestOptions` | `ErrorBehavior`, per-call `Directory`, and static `NoThrow` exactly as the public API design specifies | | `ErrorBehavior` | `Default` and `NoThrow` values used only through per-call request options | | Exception spine | Hand-written `OpenCodeException`, `OpenCodeApiException`, and `OpenCodeTransportException` with their final inheritance and payload roles | -| Response spine | Hand-written `OpenCodeResponse` base with status, `IsError`, and guarded error/success behavior | +| Response spine | Hand-written `OpenCodeResponse` base with status, `IsError`, typed error, raw error body, and guarded success behavior | | Generated clients | `Sessions`, bound `SessionClient`, `GetHealthAsync`, and `GetMessageAsync` only | | Generated data | Selected success/error models, envelopes, marked unions, unknown carriers, converter set, routes, and serializer context | @@ -434,7 +434,9 @@ models and their unknown carrier are generated. Default calls throw `OpenCodeApi the status and typed `OpenCodeError`. `NoThrow` returns `SessionMessageResponse` with `IsError = true`, the typed error populated, and guarded success payload access. An unknown future error tag becomes the generic unknown-error carrier; -the raw payload remains available. +an undeclared status or known tag at the wrong status uses the same carrier rather than widening +the operation contract. The raw body remains available on both the exception and the `NoThrow` +response. A malformed error body preserves that raw body with a null typed error. Transport failures and cancellation stay separate: network/protocol failures use the transport exception spine; cancellation follows the BCL `OperationCanceledException` convention. @@ -616,16 +618,14 @@ design and revised execution plan. - `docs/research/13-generator-complexity-checkpoint.md` - `docs/research/08-codegen-spike.md` -- `.superpowers/sdd/2026-08-11-slice-01-ingestion-specir/progress.md` - `docs/adr/0003-model-layer-codegen.md` - `docs/adr/0004-generated-model-principles.md` -- `docs/adr/0005-both-api-surfaces.md` +- `docs/adr/0005-v2-protocol-surface-only.md` - `docs/adr/0007-error-model.md` - `docs/adr/0008-generated-operation-methods.md` - `docs/adr/0009-unknown-variant-tolerance.md` - `docs/superpowers/specs/2026-08-09-generator-architecture.md` - `docs/superpowers/specs/2026-08-09-public-api-design.md` - `docs/superpowers/specs/2026-08-10-testing-architecture-design.md` -- `docs/superpowers/plans/2026-08-10-implementation-slice-map.md` -- `docs/superpowers/plans/2026-08-11-slice-01-ingestion-specir.md` +- `docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md` - `spec/openapi.json` diff --git a/spec/SNAPSHOT.md b/spec/SNAPSHOT.md index 31c6037ad..cf497e939 100644 --- a/spec/SNAPSHOT.md +++ b/spec/SNAPSHOT.md @@ -1,21 +1,28 @@ # OpenAPI Snapshot -Date: 2026-08-08 +Date: 2026-08-13 -`openapi.json` is a pinned copy of the upstream opencode OpenAPI 3.1 document. The SDK is built -against this snapshot, not against the live submodule. +`openapi.json` is a pinned copy of the upstream opencode OpenAPI 3.1 document — the v2 +protocol surface (ADR-0005). The SDK is built against this snapshot, never against a live +branch; the `v2` branch moves daily and refreshes are deliberate (milestone boundaries). | Fact | Value | |---|---| -| Upstream file | `external/opencode/packages/sdk/openapi.json` | -| Submodule commit | `d7b115f623760e68a4749d16508a9eca350f246f` | -| Upstream tag | `v1.18.15` | +| Upstream file | `packages/protocol/openapi.json` | +| Upstream branch | `v2` (active successor line; no release tags yet) | +| Commit | `a6a712a3ac72248c9b2f2f883e752e6e18ef8c40` | +| Upstream product channel | `opencode2` — npm `@opencode-ai/cli@next` (pre-release) | + +Platform evidence for the v2 line: `docs/research/15-opencode-v2-platform.md`. ## Refresh procedure -1. Update the `external/opencode` submodule to the desired upstream commit. -2. Copy `external/opencode/packages/sdk/openapi.json` over `spec/openapi.json`. -3. Update the table above (commit, tag) and the `Date:` line. -4. Review the diff; regenerate the model layer once codegen exists. +1. Pick the target `v2`-branch commit and fetch `packages/protocol/openapi.json` at exactly + that commit (read-only `git fetch` in the `external/opencode` submodule, or + `raw.githubusercontent.com` pinned to the full commit SHA — never a branch ref). +2. Copy it over `spec/openapi.json`. +3. Update the table above (commit, channel) and the `Date:` line. +4. Run `generate`; resolve what it reports (wall admits, curation rows) and review the + regenerated diff. A dedicated spec-refresh tool is planned — see repo tooling in `docs/ROADMAP.md`. diff --git a/spec/openapi.json b/spec/openapi.json index afe14bb60..4d175c8a6 100644 --- a/spec/openapi.json +++ b/spec/openapi.json @@ -1,1408 +1,1240 @@ { "openapi": "3.1.0", "info": { - "title": "opencode", - "version": "1.0.0", - "description": "opencode api" + "title": "opencode HttpApi", + "version": "0.0.1", + "description": "Experimental HttpApi surface for selected instance routes." }, "paths": { - "/auth/{providerID}": { - "put": { - "tags": ["control"], - "operationId": "auth.set", - "parameters": [ - { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], + "/api/health": { + "get": { + "tags": ["health"], + "operationId": "v2.health.get", + "parameters": [], + "security": [], "responses": { "200": { - "description": "Successfully set authentication credentials", + "description": "ServiceHealth", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Successfully set authentication credentials" + "$ref": "#/components/schemas/ServiceHealth" } } } }, "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "Set authentication credentials", - "summary": "Set auth credentials", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Auth" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["control"], - "operationId": "auth.remove", - "parameters": [ - { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "Successfully removed authentication credentials", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Successfully removed authentication credentials" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Remove authentication credentials", - "summary": "Remove auth credentials", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})" - } - ] + "description": "Report the owning server process and its application status.", + "summary": "Check server health" } }, - "/log": { + "/api/service/stop": { "post": { - "tags": ["control"], - "operationId": "app.log", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], + "tags": ["health"], + "operationId": "v2.health.stop", + "parameters": [], + "security": [], "responses": { "200": { - "description": "Log entry written successfully", + "description": "ServiceStopResponse", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Log entry written successfully" + "$ref": "#/components/schemas/ServiceStopResponse" } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Write a log entry to the server logs with specified level and metadata.", - "summary": "Write log", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "service": { - "type": "string", - "description": "Service name for the log entry" - }, - "level": { - "type": "string", - "enum": ["debug", "info", "error", "warn"], - "description": "Log level" - }, - "message": { - "type": "string", - "description": "Log message" - }, - "extra": { - "type": "object" - } - }, - "required": ["service", "level", "message"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.log({\n ...\n})" - } - ] - } - }, - "/experimental/control-plane/move-session": { - "post": { - "tags": ["controlPlane"], - "operationId": "experimental.controlPlane.moveSession", - "parameters": [], - "responses": { - "204": { - "description": "Session moved" }, - "400": { - "description": "MoveSessionError | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/MoveSessionError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Move a session to another project directory, optionally transferring local changes.", - "summary": "Move session", + "description": "Request graceful shutdown of one exact managed server instance.", + "summary": "Stop the managed server", "requestBody": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "destination": { - "$ref": "#/components/schemas/MoveSessionDestination" - }, - "moveChanges": { - "type": "boolean" - } - }, - "required": ["sessionID", "destination"], - "additionalProperties": false + "$ref": "#/components/schemas/ServiceStopRequest" } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.controlPlane.moveSession({\n ...\n})" - } - ] + }, + "required": true + } } }, - "/global/health": { + "/api/server": { "get": { - "tags": ["global"], - "operationId": "global.health", + "tags": ["server"], + "operationId": "v2.server.get", "parameters": [], + "security": [], "responses": { "200": { - "description": "Health information", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { - "healthy": { - "type": "boolean", - "enum": [true] - }, - "version": { - "type": "string" + "urls": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": ["healthy", "version"], - "additionalProperties": false, - "description": "Health information" + "required": ["urls"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get health information about the OpenCode server.", - "summary": "Get health", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.health({\n ...\n})" - } - ] - } - }, - "/global/event": { - "get": { - "tags": ["global"], - "operationId": "global.event", - "parameters": [], - "responses": { - "200": { - "description": "Event stream", - "content": { - "text/event-stream": { - "schema": { - "$ref": "#/components/schemas/GlobalEvent" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Subscribe to global events from the OpenCode system using server-sent events.", - "summary": "Get global events", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.event({\n ...\n})" - } - ] + "description": "Return the URLs that can be used to connect to this server.", + "summary": "Get server information" } }, - "/global/config": { + "/api/location": { "get": { - "tags": ["global"], - "operationId": "global.config.get", - "parameters": [], + "tags": ["location"], + "operationId": "v2.location.get", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], "responses": { "200": { - "description": "Get global config info", + "description": "Location.Info", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Config" + "$ref": "#/components/schemas/Location.Info" } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Retrieve the current global OpenCode configuration settings and preferences.", - "summary": "Get global configuration", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.get({\n ...\n})" - } - ] - }, - "patch": { - "tags": ["global"], - "operationId": "global.config.update", - "parameters": [], - "responses": { - "200": { - "description": "Successfully updated global config", + }, + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Config" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Update global OpenCode configuration settings and preferences.", - "summary": "Update global configuration", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.update({\n ...\n})" - } - ] + "description": "Resolve the requested location or the server default location.", + "summary": "Get location" } }, - "/global/dispose": { - "post": { - "tags": ["global"], - "operationId": "global.dispose", - "parameters": [], + "/api/agent": { + "get": { + "tags": ["agent"], + "operationId": "v2.agent.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], "responses": { "200": { - "description": "Global disposed", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Global disposed" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Agent.Info" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Clean up and dispose all OpenCode instances, releasing all resources.", - "summary": "Dispose instance", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.dispose({\n ...\n})" - } - ] - } - }, - "/global/upgrade": { - "post": { - "tags": ["global"], - "operationId": "global.upgrade", - "parameters": [], - "responses": { - "200": { - "description": "Upgrade result", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [true] - }, - "version": { - "type": "string" - } - }, - "required": ["success", "version"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [false] - }, - "error": { - "type": "string" - } - }, - "required": ["success", "error"], - "additionalProperties": false - } - ], - "description": "Upgrade result" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Upgrade opencode to the specified version or latest if not specified.", - "summary": "Upgrade opencode", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "target": { - "type": "string" - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.upgrade({\n ...\n})" - } - ] + "description": "Retrieve currently registered agents.", + "summary": "List agents" } }, - "/event": { + "/api/agent/{agentID}": { "get": { - "tags": ["event"], - "operationId": "event.subscribe", + "tags": ["agent"], + "operationId": "v2.agent.get", "parameters": [ { - "name": "directory", - "in": "query", + "name": "agentID", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Event stream", + "description": "Success", "content": { - "text/event-stream": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Event" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Agent.Info" + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } - } - }, - "description": "Get events", - "summary": "Subscribe to events", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.event.subscribe({\n ...\n})" - } - ] - } - }, - "/config": { - "get": { - "tags": ["config"], - "operationId": "config.get", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Get config info", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Config" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "AgentNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/AgentNotFoundError" } } } } }, - "description": "Retrieve the current OpenCode configuration settings and preferences.", - "summary": "Get configuration", - "x-codeSamples": [ + "description": "Retrieve a single currently registered agent.", + "summary": "Get agent" + } + }, + "/api/plugin": { + "get": { + "tags": ["plugin"], + "operationId": "v2.plugin.list", + "parameters": [ { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.get({\n ...\n})" - } - ] - }, - "patch": { - "tags": ["config"], - "operationId": "config.update", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Successfully updated config", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Config" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Plugin.Info" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Update OpenCode configuration settings and preferences.", - "summary": "Update configuration", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } } } }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.update({\n ...\n})" - } - ] + "description": "Retrieve currently loaded plugins.", + "summary": "List plugins" } }, - "/config/providers": { + "/api/session": { "get": { - "tags": ["config"], - "operationId": "config.providers", + "tags": ["session"], + "operationId": "v2.session.list", "parameters": [ { - "name": "directory", + "name": "workspace", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + { + "type": "null" + } + ] }, "required": false }, { - "name": "workspace", + "name": "limit", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Maximum number of sessions to return. Defaults to the newest 50 sessions." }, "required": false - } - ], - "responses": { - "200": { - "description": "List of providers", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Provider" - } + }, + { + "name": "order", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": ["asc", "desc"] + }, + { + "type": "null" + } + ], + "description": "Session order for the first page. Use desc for newest first or asc for oldest first." + }, + "required": false + }, + { + "name": "search", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "parentID", + "in": "query", + "schema": { + "anyOf": [ + { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "default": { - "type": "object", - "additionalProperties": { - "type": "string" - } + { + "type": "string", + "enum": ["null"] } - }, - "required": ["providers", "default"], - "additionalProperties": false, - "description": "List of providers" + ], + "description": "Filter by parent session. Use null to return only root sessions." + }, + { + "type": "null" } - } - } + ] + }, + "required": false }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" + { + "name": "directory", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } - } - } - }, - "description": "Get a list of all configured AI providers and their default models.", - "summary": "List config providers", - "x-codeSamples": [ + ] + }, + "required": false + }, { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.providers({\n ...\n})" - } - ] - } - }, - "/experimental/capabilities": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.capabilities.get", - "parameters": [ + "name": "project", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, { - "name": "directory", + "name": "subpath", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "required": false }, { - "name": "workspace", + "name": "cursor", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string", + "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response." + }, + { + "type": "null" + } + ] }, "required": false } ], + "security": [], "responses": { "200": { - "description": "Experimental capabilities", + "description": "SessionsResponse", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExperimentalCapabilities" + "$ref": "#/components/schemas/SessionsResponse" } } } }, "400": { - "description": "Bad request", + "description": "InvalidCursorError | InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidCursorError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } - } - }, - "description": "Get experimental features enabled on the OpenCode server.", - "summary": "Get experimental capabilities", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.capabilities.get({\n ...\n})" - } - ] - } - }, - "/experimental/console": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.console.get", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } } - ], + }, + "description": "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", + "summary": "List sessions" + }, + "post": { + "tags": ["session"], + "operationId": "v2.session.create", + "parameters": [], + "security": [], "responses": { "200": { - "description": "Active Console provider metadata", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConsoleState" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Info" + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "500": { - "description": "InternalServerError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_InternalServerError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Get the active Console org name and the set of provider IDs managed by that Console org.", - "summary": "Get active Console provider metadata", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.console.get({\n ...\n})" - } - ] + "description": "Create a session at the requested location.", + "summary": "Create session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + { + "type": "null" + } + ] + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/Location.Ref" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/experimental/console/orgs": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.console.listOrgs", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], + "/api/session/import": { + "post": { + "tags": ["session"], + "operationId": "v2.session.import", + "parameters": [], + "security": [], "responses": { "200": { - "description": "Switchable Console orgs", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { - "orgs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "accountID": { - "type": "string" - }, - "accountEmail": { - "type": "string" - }, - "accountUrl": { - "type": "string" - }, - "orgID": { - "type": "string" - }, - "orgName": { - "type": "string" - }, - "active": { - "type": "boolean" - } - }, - "required": ["accountID", "accountEmail", "accountUrl", "orgID", "orgName", "active"], - "additionalProperties": false - } + "data": { + "$ref": "#/components/schemas/Session.Info" } }, - "required": ["orgs"], - "additionalProperties": false, - "description": "Switchable Console orgs" + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "500": { - "description": "InternalServerError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_InternalServerError" + "$ref": "#/components/schemas/UnauthorizedError" } } } - } - }, - "description": "Get the available Console orgs across logged-in accounts, including the current active org.", - "summary": "List switchable Console orgs", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.console.listOrgs({\n ...\n})" - } - ] - } - }, - "/experimental/console/switch": { - "post": { - "tags": ["experimental"], - "operationId": "experimental.console.switchOrg", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Switch success", + "409": { + "description": "ConflictError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Switch success" + "$ref": "#/components/schemas/ConflictError" } } } } }, - "description": "Persist a new active Console account/org selection for the current local OpenCode state.", - "summary": "Switch active Console org", + "description": "Import a projected session transcript at the requested location.", + "summary": "Import session", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "accountID": { - "type": "string" + "info": { + "$ref": "#/components/schemas/Session.Info" }, - "orgID": { - "type": "string" + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message.Info" + } + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/Location.Ref" + }, + { + "type": "null" + } + ] } }, - "required": ["accountID", "orgID"], + "required": ["info", "messages"], "additionalProperties": false } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.console.switchOrg({\n ...\n})" - } - ] + }, + "required": true + } } }, - "/experimental/tool": { + "/api/session/{sessionID}/export": { "get": { - "tags": ["experimental"], - "operationId": "tool.list", + "tags": ["session"], + "operationId": "v2.session.export", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "provider", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true }, { - "name": "model", + "name": "sanitize", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string", + "enum": ["true", "false"] + }, + { + "type": "null" + } + ] }, - "required": true + "required": false } ], + "security": [], "responses": { "200": { - "description": "Tools", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ToolList" - } - } + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionTransfer.Data" + } + }, + "required": ["data"], + "additionalProperties": false + } + } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionNotFoundError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" } } } } }, - "description": "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.", - "summary": "List tools", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tool.list({\n ...\n})" - } - ] + "description": "Export a complete projected session transcript.", + "summary": "Export session" } }, - "/experimental/tool/ids": { + "/api/session/active": { "get": { - "tags": ["experimental"], - "operationId": "tool.ids", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], + "tags": ["session"], + "operationId": "v2.session.active", + "parameters": [], + "security": [], "responses": { "200": { - "description": "Tool IDs", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ToolIDs" + "type": "object", + "properties": { + "data": { + "type": "object", + "patternProperties": { + "^ses": { + "$ref": "#/components/schemas/SessionActive" + } + } + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", - "summary": "List tool IDs", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tool.ids({\n ...\n})" - } - ] + "description": "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive.", + "summary": "List active sessions" } }, - "/experimental/worktree": { + "/api/session/{sessionID}": { "get": { - "tags": ["experimental"], - "operationId": "worktree.list", + "tags": ["session"], + "operationId": "v2.session.get", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "List of worktree directories", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "string" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Info" + } }, - "description": "List of worktree directories" + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "WorktreeError | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorktreeError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "List all sandbox worktrees for the current project.", - "summary": "List worktrees", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["experimental"], - "operationId": "worktree.create", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Worktree created", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Worktree" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "WorktreeError | InvalidRequestError", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorktreeError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } } }, - "description": "Create a new git worktree for the current project and run any configured startup scripts.", - "summary": "Create worktree", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorktreeCreateInput" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.create({\n ...\n})" - } - ] + "description": "Retrieve a session by ID.", + "summary": "Get session" }, "delete": { - "tags": ["experimental"], - "operationId": "worktree.remove", + "tags": ["session"], + "operationId": "v2.session.remove", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "Worktree removed", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Worktree removed" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "WorktreeError | InvalidRequestError", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/WorktreeError" + "$ref": "#/components/schemas/SessionNotFoundError" }, { - "$ref": "#/components/schemas/InvalidRequestError" + "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -1410,67 +1242,56 @@ } } }, - "description": "Remove a git worktree and delete its branch.", - "summary": "Remove worktree", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorktreeRemoveInput" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.remove({\n ...\n})" - } - ] + "description": "Delete a session and its child sessions.", + "summary": "Delete session" } }, - "/experimental/worktree/reset": { + "/api/session/{sessionID}/fork": { "post": { - "tags": ["experimental"], - "operationId": "worktree.reset", + "tags": ["session"], + "operationId": "v2.session.fork", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "Worktree reset", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Worktree reset" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Info" + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "WorktreeError | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/WorktreeError" + "$ref": "#/components/schemas/InvalidRequestError1" }, { "$ref": "#/components/schemas/InvalidRequestError" @@ -1479,203 +1300,197 @@ } } } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | MessageNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } } }, - "description": "Reset a worktree branch to the primary default branch.", - "summary": "Reset worktree", + "description": "Create a child session by copying projected history through or before a message boundary.", + "summary": "Fork session", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorktreeResetInput" + "type": "object", + "properties": { + "boundary": { + "$ref": "#/components/schemas/Session.ForkRequestBoundary" + } + }, + "required": ["boundary"], + "additionalProperties": false } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.reset({\n ...\n})" - } - ] + }, + "required": true + } } }, - "/experimental/session": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.session.list", + "/api/session/{sessionID}/agent": { + "post": { + "tags": ["session"], + "operationId": "v2.session.switchAgent", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "roots", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "enum": ["true", "false"] - } - ] - }, - "required": false - }, - { - "name": "start", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "cursor", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "archived", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "anyOf": [ - { - "type": "boolean" - }, + "type": "string", + "allOf": [ { - "type": "string", - "enum": ["true", "false"] + "pattern": "^ses" } ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "List of sessions", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GlobalSession" - }, - "description": "List of sessions" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.", - "summary": "List sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.session.list({\n ...\n})" - } - ] + "description": "Switch the agent used by subsequent provider turns.", + "summary": "Switch session agent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "type": "string" + } + }, + "required": ["agent"], + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/experimental/session/{sessionID}/background": { + "/api/session/{sessionID}/model": { "post": { - "tags": ["experimental"], - "operationId": "experimental.session.background", + "tags": ["session"], + "operationId": "v2.session.switchModel", "parameters": [ { "name": "sessionID", "in": "path", "schema": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false } ], + "security": [], "responses": { - "200": { - "description": "Backgrounded subagents", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Backgrounded subagents" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/SessionNotFoundError" }, { - "$ref": "#/components/schemas/InvalidRequestError" + "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -1683,3240 +1498,3049 @@ } } }, - "description": "Detach any synchronous subagents currently blocking the session and continue them in the background.", - "summary": "Background subagents", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.session.background({\n ...\n})" - } - ] - } - }, - "/experimental/resource": { - "get": { - "tags": ["experimental"], - "operationId": "experimental.resource.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false + "description": "Switch the model used by subsequent provider turns.", + "summary": "Switch session model", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "model": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": ["model"], + "additionalProperties": false + } + } }, + "required": true + } + } + }, + "/api/session/{sessionID}/rename": { + "post": { + "tags": ["session"], + "operationId": "v2.session.rename", + "parameters": [ { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "MCP resources", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/McpResource" - }, - "description": "MCP resources" - } - } - } + "204": { + "description": "" }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Get all available MCP resources from connected servers. Optionally filter by name.", - "summary": "Get MCP resources", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.resource.list({\n ...\n})" - } - ] - } - }, - "/find": { - "get": { - "tags": ["file"], - "operationId": "find.text", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "pattern", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "Matches", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "path": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - }, - "lines": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - }, - "line_number": { - "type": "integer", - "minimum": 0 - }, - "absolute_offset": { - "type": "integer", - "minimum": 0 - }, - "submatches": { - "type": "array", - "items": { - "type": "object", - "properties": { - "match": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - }, - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["match", "start", "end"], - "additionalProperties": false - } - } - }, - "required": ["path", "lines", "line_number", "absolute_offset", "submatches"], - "additionalProperties": false - }, - "description": "Matches" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "Bad request", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Search for text patterns across files in the project using ripgrep.", - "summary": "Find text", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.find.text({\n ...\n})" - } - ] + "description": "Update the session title.", + "summary": "Rename session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": ["title"], + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/find/file": { - "get": { - "tags": ["file"], - "operationId": "find.files", + "/api/session/{sessionID}/move": { + "post": { + "tags": ["session"], + "operationId": "v2.session.move", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "query", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "dirs", - "in": "query", - "schema": { - "type": "string", - "enum": ["true", "false"] - }, - "required": false - }, - { - "name": "type", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { "type": "string", - "enum": ["file", "directory"] - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 200 + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "File paths", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "string" - }, - "description": "File paths" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/SessionNotFoundError" } } } } }, - "description": "Search for files or directories by name or pattern in the project directory.", - "summary": "Find files", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.find.files({\n ...\n})" - } - ] - } - }, - "/find/symbol": { - "get": { - "tags": ["file"], - "operationId": "find.symbols", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false + "description": "Move a session to another project directory, optionally transferring local changes.", + "summary": "Move session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + "delivery": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + }, + { + "type": "null" + } + ] + } + }, + "required": ["directory"], + "additionalProperties": false + } + } }, + "required": true + } + } + }, + "/api/session/{sessionID}/prompt": { + "post": { + "tags": ["session"], + "operationId": "v2.session.prompt", + "parameters": [ { - "name": "query", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true } ], + "security": [], "responses": { "200": { - "description": "Symbols", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Symbol" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Inbox.User" + } }, - "description": "Symbols" + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } - } - }, - "description": "Search for workspace symbols like functions, classes, and variables using LSP.", - "summary": "Find symbols", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.find.symbols({\n ...\n})" - } - ] - } - }, - "/file": { - "get": { - "tags": ["file"], - "operationId": "file.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } }, - { - "name": "path", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "Files and directories", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileNode" - }, - "description": "Files and directories" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } }, - "400": { - "description": "Bad request", + "409": { + "description": "ConflictError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/ConflictError" } } } } }, - "description": "List files and directories in a specified path.", - "summary": "List files", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.file.list({\n ...\n})" - } - ] + "description": "Durably admit one session input and schedule agent-loop execution unless resume is false.", + "summary": "Send message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.SkillAttachment" + } + }, + "metadata": { + "type": "object" + }, + "delivery": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": ["text"], + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/file/content": { - "get": { - "tags": ["file"], - "operationId": "file.read", + "/api/session/{sessionID}/command": { + "post": { + "tags": ["session"], + "operationId": "v2.session.command", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "path", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true } ], + "security": [], "responses": { "200": { - "description": "File content", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileContent" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Inbox.User" + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } - } - }, - "description": "Read the content of a specified file.", - "summary": "Read file", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.file.read({\n ...\n})" - } - ] - } - }, - "/file/status": { - "get": { - "tags": ["file"], - "operationId": "file.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "File status", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/File" - }, - "description": "File status" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "Bad request", + "404": { + "description": "SessionNotFoundError | CommandNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/CommandNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } - } - }, - "description": "Get the git status of all files in the project.", - "summary": "Get file status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.file.status({\n ...\n})" - } - ] - } - }, - "/instance/dispose": { - "post": { - "tags": ["instance"], - "operationId": "instance.dispose", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Instance disposed", + "409": { + "description": "ConflictError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Instance disposed" + "$ref": "#/components/schemas/ConflictError" } } } }, - "400": { - "description": "Bad request", + "500": { + "description": "CommandEvaluationError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/CommandEvaluationError" } } } } }, - "description": "Clean up and dispose the current OpenCode instance, releasing all resources.", - "summary": "Dispose instance", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.instance.dispose({\n ...\n})" - } - ] + "description": "Resolve a slash command into prompt input, admit it durably, and schedule execution unless resume is false.", + "summary": "Run command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "command": { + "type": "string" + }, + "arguments": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.SkillAttachment" + } + }, + "delivery": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": ["command"], + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/path": { - "get": { - "tags": ["instance"], - "operationId": "path.get", + "/api/session/{sessionID}/skill": { + "post": { + "tags": ["session"], + "operationId": "v2.session.skill", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "Path", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Path" - } - } - } + "204": { + "description": "" }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Retrieve the current working directory and related path information for the OpenCode instance.", - "summary": "Get paths", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.path.get({\n ...\n})" - } - ] - } - }, - "/vcs": { - "get": { - "tags": ["instance"], - "operationId": "vcs.get", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "VCS info", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VcsInfo" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "Bad request", + "404": { + "description": "SessionNotFoundError | SkillNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/SkillNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Retrieve version control system (VCS) information for the current project, such as git branch.", - "summary": "Get VCS info", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.get({\n ...\n})" - } - ] - } - }, - "/vcs/status": { - "get": { - "tags": ["instance"], - "operationId": "vcs.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "VCS status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VcsFileStatus" + "description": "Activate a skill for a session by appending a skill message and resuming execution.", + "summary": "Activate skill", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] }, - "description": "VCS status" - } + "skill": { + "type": "string" + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": ["skill"], + "additionalProperties": false } } }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve changed files in the current working tree without patches.", - "summary": "Get VCS status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.status({\n ...\n})" - } - ] + "required": true + } } }, - "/vcs/diff": { - "get": { - "tags": ["instance"], - "operationId": "vcs.diff", + "/api/session/{sessionID}/synthetic": { + "post": { + "tags": ["session"], + "operationId": "v2.session.synthetic", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "mode", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { "type": "string", - "enum": ["git", "branch"] + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true - }, - { - "name": "context", - "in": "query", - "schema": { - "type": "integer", - "minimum": 0 - }, - "required": false } ], + "security": [], "responses": { "200": { - "description": "VCS diff", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VcsFileDiff" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Inbox.Synthetic" + } }, - "description": "VCS diff" + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Retrieve the current git diff for the working tree or against the default branch.", - "summary": "Get VCS diff", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.diff({\n ...\n})" - } - ] - } - }, - "/vcs/diff/raw": { - "get": { - "tags": ["instance"], - "operationId": "vcs.diff.raw", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Raw VCS diff", - "content": { - "text/x-diff; charset=utf-8": { - "schema": { - "type": "string" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } - } - }, - "description": "Retrieve a raw patch for current uncommitted changes.", - "summary": "Get raw VCS diff", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.diff.raw({\n ...\n})" - } - ] - } - }, - "/vcs/apply": { - "post": { - "tags": ["instance"], - "operationId": "vcs.apply", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "VCS patch applied", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "applied": { - "type": "boolean" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" } - }, - "required": ["applied"], - "additionalProperties": false, - "description": "VCS patch applied" + ] } } } }, - "400": { - "description": "VcsApplyError | InvalidRequestError", + "409": { + "description": "ConflictError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/VcsApplyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/ConflictError" } } } } }, - "description": "Apply a raw patch to the current working tree.", - "summary": "Apply VCS patch", + "description": "Durably admit synthetic session input and schedule execution unless resume is false.", + "summary": "Add synthetic message", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "patch": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "text": { "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "type": "object" + }, + "delivery": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] } }, - "required": ["patch"], + "required": ["text"], "additionalProperties": false } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.apply({\n ...\n})" - } - ] + }, + "required": true + } } }, - "/command": { - "get": { - "tags": ["instance"], - "operationId": "command.list", + "/api/session/{sessionID}/shell": { + "post": { + "tags": ["session"], + "operationId": "v2.session.shell", "parameters": [ { - "name": "directory", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "List of commands", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Command" - }, - "description": "List of commands" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Get a list of all available commands in the OpenCode system.", - "summary": "List commands", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.command.list({\n ...\n})" - } - ] + "description": "Execute one shell command in the session's working directory. Emits a shell.started event before execution and a shell.ended event with the merged output after.", + "summary": "Run shell command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + { + "type": "null" + } + ] + }, + "command": { + "type": "string" + } + }, + "required": ["command"], + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/agent": { - "get": { - "tags": ["instance"], - "operationId": "app.agents", + "/api/session/{sessionID}/compact": { + "post": { + "tags": ["session"], + "operationId": "v2.session.compact", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "List of agents", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Agent" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Inbox.Compaction" + } }, - "description": "List of agents" + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Get a list of all available AI agents in the OpenCode system.", - "summary": "List agents", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.agents({\n ...\n})" - } - ] - } - }, - "/skill": { - "get": { - "tags": ["instance"], - "operationId": "app.skills", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of skills", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "location": { - "type": "string" - }, - "content": { - "type": "string" - } + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" }, - "required": ["name", "location", "content"], - "additionalProperties": false - }, - "description": "List of skills" + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } }, - "400": { - "description": "Bad request", + "409": { + "description": "ConflictError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/ConflictError" } } } } }, - "description": "Get a list of all available skills in the OpenCode system.", - "summary": "List skills", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.skills({\n ...\n})" - } - ] + "description": "Queue a durable session compaction request.", + "summary": "Compact session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "delivery": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/lsp": { - "get": { - "tags": ["instance"], - "operationId": "lsp.status", + "/api/session/{sessionID}/wait": { + "post": { + "tags": ["session"], + "operationId": "v2.session.wait", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "LSP server status", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LSPStatus" - }, - "description": "LSP server status" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } - } - }, - "description": "Get LSP server status", - "summary": "Get LSP status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.lsp.status({\n ...\n})" - } - ] - } - }, - "/formatter": { - "get": { - "tags": ["instance"], - "operationId": "formatter.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Formatter status", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FormatterStatus" - }, - "description": "Formatter status" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } }, - "400": { - "description": "Bad request", + "503": { + "description": "ServiceUnavailableError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/ServiceUnavailableError" } } } } }, - "description": "Get formatter status", - "summary": "Get formatter status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.formatter.status({\n ...\n})" - } - ] + "description": "Wait for a session agent loop to become idle.", + "summary": "Wait for session" } }, - "/mcp": { - "get": { - "tags": ["mcp"], - "operationId": "mcp.status", + "/api/session/{sessionID}/revert/stage": { + "post": { + "tags": ["session"], + "operationId": "v2.session.revert.stage", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "MCP server status", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/MCPStatus" + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Revert" + } }, - "description": "MCP server status" + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Get the status of all Model Context Protocol (MCP) servers.", - "summary": "Get MCP status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.status({\n ...\n})" - } - ] - }, - "post": { - "tags": ["mcp"], - "operationId": "mcp.add", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "MCP server added successfully", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/MCPStatus" - }, - "description": "MCP server added successfully" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "404": { + "description": "MessageNotFoundError | SessionNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/MessageNotFoundError" }, { - "$ref": "#/components/schemas/InvalidRequestError" + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" } ] } } } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } } }, - "description": "Dynamically add a new Model Context Protocol (MCP) server to the system.", - "summary": "Add MCP server", + "description": "Stage or move a reversible session boundary and optionally apply its file changes.", + "summary": "Stage session revert", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "name": { - "type": "string" + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "config": { + "files": { "anyOf": [ { - "$ref": "#/components/schemas/McpLocalConfig" + "type": "boolean" }, { - "$ref": "#/components/schemas/McpRemoteConfig" + "type": "null" } ] } }, - "required": ["name", "config"], + "required": ["messageID"], "additionalProperties": false } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.add({\n ...\n})" - } - ] + }, + "required": true + } } }, - "/mcp/{name}/auth": { + "/api/session/{sessionID}/revert/clear": { "post": { - "tags": ["mcp"], - "operationId": "mcp.auth.start", + "tags": ["session"], + "operationId": "v2.session.revert.clear", "parameters": [ { - "name": "name", + "name": "sessionID", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false } ], + "security": [], "responses": { - "200": { - "description": "OAuth flow started", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "authorizationUrl": { - "type": "string" - }, - "oauthState": { - "type": "string" - } - }, - "required": ["authorizationUrl", "oauthState"], - "additionalProperties": false, - "description": "OAuth flow started" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "McpUnsupportedOAuthError | InvalidRequestError", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/McpUnsupportedOAuthError" + "$ref": "#/components/schemas/SessionNotFoundError" }, { - "$ref": "#/components/schemas/InvalidRequestError" + "$ref": "#/components/schemas/SessionNotFoundError" } ] } } } }, - "404": { - "description": "McpServerNotFoundError", + "409": { + "description": "SessionBusyError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" } } } } }, - "description": "Start OAuth authentication flow for a Model Context Protocol (MCP) server.", - "summary": "Start MCP OAuth", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.start({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["mcp"], - "operationId": "mcp.auth.remove", + "summary": "Clear staged revert" + } + }, + "/api/session/{sessionID}/revert/commit": { + "post": { + "tags": ["session"], + "operationId": "v2.session.revert.commit", "parameters": [ { - "name": "name", + "name": "sessionID", "in": "path", "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "OAuth credentials removed", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [true] - } - }, - "required": ["success"], - "additionalProperties": false, - "description": "OAuth credentials removed" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "McpServerNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" } } } } }, - "description": "Remove OAuth credentials for an MCP server.", - "summary": "Remove MCP OAuth", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.remove({\n ...\n})" - } - ] + "summary": "Commit staged revert" } }, - "/mcp/{name}/auth/callback": { - "post": { - "tags": ["mcp"], - "operationId": "mcp.auth.callback", + "/api/session/{sessionID}/context": { + "get": { + "tags": ["session"], + "operationId": "v2.session.context", "parameters": [ { - "name": "name", + "name": "sessionID", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false } ], + "security": [], "responses": { "200": { - "description": "OAuth authentication completed", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MCPStatus" + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message.Info" + } + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "McpServerNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" } } } - } - }, - "description": "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.", - "summary": "Complete MCP OAuth", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": { - "type": "string" - } - }, - "required": ["code"], - "additionalProperties": false + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } } } } }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.callback({\n ...\n})" - } - ] + "description": "Retrieve the active context messages for a session (all messages after the last compaction).", + "summary": "Get session context" } }, - "/mcp/{name}/auth/authenticate": { - "post": { - "tags": ["mcp"], - "operationId": "mcp.auth.authenticate", + "/api/session/{sessionID}/inbox": { + "get": { + "tags": ["session"], + "operationId": "v2.session.inbox.list", "parameters": [ { - "name": "name", + "name": "sessionID", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false } ], + "security": [], "responses": { "200": { - "description": "OAuth authentication completed", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MCPStatus" + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Inbox.Info" + } + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "McpUnsupportedOAuthError | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/McpUnsupportedOAuthError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "McpServerNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" } } } } }, - "description": "Start OAuth flow and wait for callback (opens browser).", - "summary": "Authenticate MCP OAuth", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.authenticate({\n ...\n})" - } - ] + "description": "List durable enqueued session work not yet delivered, ordered by enqueue sequence. Includes user, synthetic, compaction, and move items.", + "summary": "List session inbox" } }, - "/mcp/{name}/connect": { - "post": { - "tags": ["mcp"], - "operationId": "mcp.connect", + "/api/session/{sessionID}/inbox/{inboxID}": { + "delete": { + "tags": ["session"], + "operationId": "v2.session.inbox.cancel", "parameters": [ { - "name": "name", + "name": "sessionID", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true }, { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "inboxID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "MCP server connected successfully", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "MCP server connected successfully" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "McpServerNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" } } } - } - }, - "description": "Connect an MCP server.", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.connect({\n ...\n})" - } - ] + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Cancel an inbox item that has not yet been delivered.", + "summary": "Cancel inbox input" } }, - "/mcp/{name}/disconnect": { + "/api/session/{sessionID}/inbox/{inboxID}/steer": { "post": { - "tags": ["mcp"], - "operationId": "mcp.disconnect", + "tags": ["session"], + "operationId": "v2.session.inbox.steer", "parameters": [ { - "name": "name", + "name": "sessionID", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true }, { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "inboxID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "MCP server disconnected successfully", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "MCP server disconnected successfully" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "McpServerNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" } } } } }, - "description": "Disconnect an MCP server.", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.disconnect({\n ...\n})" - } - ] + "description": "Change a queued inbox item to steer delivery and wake session execution.", + "summary": "Steer queued item" } }, - "/project": { - "get": { - "tags": ["project"], - "operationId": "project.list", + "/api/session/{sessionID}/inbox/{inboxID}/queue": { + "post": { + "tags": ["session"], + "operationId": "v2.session.inbox.queue", "parameters": [ { - "name": "directory", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true }, { - "name": "workspace", - "in": "query", + "name": "inboxID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "List of projects", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Project" - }, - "description": "List of projects" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionNotFoundError" + } + } + } + }, + "409": { + "description": "ConflictError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/ConflictError" } } } } }, - "description": "Get a list of projects that have been opened with OpenCode.", - "summary": "List all projects", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.list({\n ...\n})" - } - ] + "description": "Change a steered inbox item to queued delivery.", + "summary": "Queue steered item" } }, - "/project/current": { + "/api/session/{sessionID}/instructions/entries": { "get": { - "tags": ["project"], - "operationId": "project.current", + "tags": ["session"], + "operationId": "v2.session.instructions.entry.list", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "Current project information", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Project" + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InstructionEntry.Info" + } + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Retrieve the currently active project that OpenCode is working with.", - "summary": "Get current project", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.current({\n ...\n})" - } - ] - } - }, - "/project/git/init": { - "post": { - "tags": ["project"], - "operationId": "project.initGit", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Project information after git initialization", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Project" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "Bad request", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Create a git repository for the current project and return the refreshed project info.", - "summary": "Initialize git repository", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.initGit({\n ...\n})" - } - ] + "description": "List API-managed instruction entries attached to the session.", + "summary": "List instruction entries" } }, - "/project/{projectID}": { - "patch": { - "tags": ["project"], - "operationId": "project.update", + "/api/session/{sessionID}/instructions/entries/{key}": { + "put": { + "tags": ["session"], + "operationId": "v2.session.instructions.entry.put", "parameters": [ { - "name": "projectID", + "name": "sessionID", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true }, { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "key", + "in": "path", "schema": { - "type": "string" + "$ref": "#/components/schemas/InstructionEntry.Key" }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "Updated project information", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Project" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "ProjectNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProjectNotFoundError" - } - } + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "413": { + "description": "InstructionEntryValueTooLargeError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstructionEntryValueTooLargeError" + } + } } } }, - "description": "Update project properties such as name, icon, and commands.", - "summary": "Update project", + "description": "Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.", + "summary": "Put instruction entry", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" - } + "value": {} }, + "required": ["value"], "additionalProperties": false } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.update({\n ...\n})" - } - ] - } - }, - "/project/{projectID}/directories": { - "get": { - "tags": ["project"], - "operationId": "project.directories", + }, + "required": true + } + }, + "delete": { + "tags": ["session"], + "operationId": "v2.session.instructions.entry.remove", "parameters": [ { - "name": "projectID", + "name": "sessionID", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true }, { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "key", + "in": "path", "schema": { - "type": "string" + "$ref": "#/components/schemas/InstructionEntry.Key" }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "Project directories", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProjectDirectories" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "List known local absolute directories for a project.", - "summary": "List project directories", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.directories({\n ...\n})" - } - ] + "description": "Remove one instruction entry; the removal is announced to the model at the next step boundary.", + "summary": "Remove instruction entry" } }, - "/experimental/project/{projectID}/copy/generate-name": { + "/api/session/{sessionID}/generate": { "post": { - "tags": ["projectCopy"], - "operationId": "experimental.projectCopy.generateName", + "tags": ["session"], + "operationId": "v2.session.generate", "parameters": [ { - "name": "projectID", + "name": "sessionID", "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false } ], + "security": [], "responses": { "200": { - "description": "Success", + "description": "SessionGenerateResponse", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false + "$ref": "#/components/schemas/SessionGenerateResponse" } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Generate a short name for a project copy from task context.", - "summary": "Generate project copy name", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "context": { - "type": "string" - } - }, - "additionalProperties": false + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.projectCopy.generateName({\n ...\n})" - } - ] - } - }, - "/pty/shells": { - "get": { - "tags": ["pty"], - "operationId": "pty.shells", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of shells", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "name": { - "type": "string" - }, - "acceptable": { - "type": "boolean" - } + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" }, - "required": ["path", "name", "acceptable"], - "additionalProperties": false - }, - "description": "List of shells" + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } }, - "400": { - "description": "Bad request", + "503": { + "description": "ServiceUnavailableError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/ServiceUnavailableError" } } } } }, - "description": "Get a list of available shells on the system.", - "summary": "List available shells", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.shells({\n ...\n})" - } - ] + "description": "Generate transient text from the current session context without mutating session history.", + "summary": "Generate text from session context", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + }, + "required": ["prompt"], + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/pty": { + "/api/experimental/session/{sessionID}/log": { "get": { - "tags": ["pty"], - "operationId": "pty.list", + "tags": ["session"], + "operationId": "v2.session.log", "parameters": [ { - "name": "directory", + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "after", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "required": false }, { - "name": "workspace", + "name": "follow", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string", + "enum": ["true", "false"] + }, + { + "type": "null" + } + ] }, "required": false } ], + "security": [], "responses": { "200": { - "description": "List of sessions", + "description": "Success", "content": { - "application/json": { + "text/event-stream": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pty" + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/SessionLogItemJsonString" + } + }, + "required": ["id", "event", "data"], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["Fail"] + }, + "error": { + "not": {} + } + }, + "required": ["_tag", "error"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["Die"] + }, + "defect": {} + }, + "required": ["_tag", "defect"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["Interrupt"] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": ["_tag", "fiberId"], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} }, - "description": "List of sessions" + "failureEvent": "effect/httpapi/stream/failure" } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.", - "summary": "List PTY sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["pty"], - "operationId": "pty.create", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Created session", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Pty" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } } }, - "description": "Create a new pseudo-terminal (PTY) session for running shell commands and processes.", - "summary": "Create PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "title": { - "type": "string" - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.create({\n ...\n})" - } - ] + "description": "Experimental durable session event log. Reads events after an exclusive aggregate sequence and continues with live events when follow=true.", + "summary": "Read the session log" } }, - "/pty/{ptyID}": { - "get": { - "tags": ["pty"], - "operationId": "pty.get", + "/api/session/{sessionID}/interrupt": { + "post": { + "tags": ["session"], + "operationId": "v2.session.interrupt", "parameters": [ { - "name": "ptyID", + "name": "sessionID", "in": "path", "schema": { "type": "string", - "pattern": "^pty" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true }, { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "continue", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string", + "enum": ["true", "false"] + }, + { + "type": "null" + } + ] }, "required": false } ], + "security": [], "responses": { - "200": { - "description": "Session info", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Pty" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "PtyNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Retrieve detailed information about a specific pseudo-terminal (PTY) session.", - "summary": "Get PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.get({\n ...\n})" - } - ] - }, - "put": { - "tags": ["pty"], - "operationId": "pty.update", + "description": "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op. When continue=true, execution resumes if durable inbox work remains after interruption.", + "summary": "Interrupt session execution" + } + }, + "/api/session/{sessionID}/background": { + "post": { + "tags": ["session"], + "operationId": "v2.session.background", "parameters": [ { - "name": "ptyID", + "name": "sessionID", "in": "path", "schema": { "type": "string", - "pattern": "^pty" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false } ], + "security": [], "responses": { - "200": { - "description": "Updated session", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Pty" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "PtyNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Update properties of an existing pseudo-terminal (PTY) session.", - "summary": "Update PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "size": { - "type": "object", - "properties": { - "rows": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "cols": { - "type": "integer", - "exclusiveMinimum": 0 - } + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" }, - "required": ["rows", "cols"], - "additionalProperties": false - } - }, - "additionalProperties": false + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } } } } }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.update({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["pty"], - "operationId": "pty.remove", + "description": "Move active foreground backgroundable tools for this session into background observation. Idle requests are a no-op.", + "summary": "Background blocking session tools" + } + }, + "/api/session/{sessionID}/message/{messageID}": { + "get": { + "tags": ["session"], + "operationId": "v2.session.message", "parameters": [ { - "name": "ptyID", + "name": "sessionID", "in": "path", "schema": { "type": "string", - "pattern": "^pty" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true }, { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "messageID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "Session removed", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Session removed" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Message.Info" + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "PtyNotFoundError", + "description": "SessionNotFoundError | MessageNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/MessageNotFoundError" + } + ] } } } } }, - "description": "Remove and terminate a specific pseudo-terminal (PTY) session.", - "summary": "Remove PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.remove({\n ...\n})" - } - ] + "description": "Retrieve one projected message owned by the Session.", + "summary": "Get session message" } }, - "/pty/{ptyID}/connect-token": { - "post": { - "tags": ["pty"], - "operationId": "pty.connectToken", + "/api/session/{sessionID}/message": { + "get": { + "tags": ["session"], + "operationId": "v2.message.list", "parameters": [ { - "name": "ptyID", + "name": "sessionID", "in": "path", "schema": { "type": "string", - "pattern": "^pty" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true }, { - "name": "directory", + "name": "limit", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return. When omitted, the endpoint returns its default page size." }, "required": false }, { - "name": "workspace", + "name": "order", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string", + "enum": ["asc", "desc"] + }, + { + "type": "null" + } + ], + "description": "Message order for the first page. Use desc for newest first or asc for oldest first." + }, + "required": false + }, + { + "name": "cursor", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order." + }, + { + "type": "null" + } + ] }, "required": false } ], + "security": [], "responses": { "200": { - "description": "WebSocket connect token", + "description": "SessionMessagesResponse", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyTicketConnectToken" + "$ref": "#/components/schemas/SessionMessagesResponse" } } } }, "400": { - "description": "Bad request", + "description": "InvalidCursorError | InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidCursorError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } }, - "403": { - "description": "PtyForbiddenError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyForbiddenError" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "PtyNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" } } } } }, - "description": "Create a short-lived ticket for opening a PTY WebSocket connection.", - "summary": "Create PTY WebSocket token", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.connectToken({\n ...\n})" - } - ] + "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", + "summary": "Get session messages" } }, - "/question": { + "/api/model": { "get": { - "tags": ["question"], - "operationId": "question.list", + "tags": ["model"], + "operationId": "v2.model.list", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "List of pending questions", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionRequest" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Info" + } + } }, - "description": "List of pending questions" + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get all pending question requests across all sessions.", - "summary": "List pending questions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.list({\n ...\n})" - } - ] - } - }, - "/question/{requestID}/reply": { - "post": { - "tags": ["question"], - "operationId": "question.reply", - "parameters": [ - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^que" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Question answered successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Question answered successfully" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "404": { - "description": "QuestionNotFoundError", + "503": { + "description": "ServiceUnavailableError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionNotFoundError" + "$ref": "#/components/schemas/ServiceUnavailableError" } } } } }, - "description": "Provide answers to a question request from the AI assistant.", - "summary": "Reply to question request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - }, - "description": "User answers in order of questions (each answer is an array of selected labels)" - } - }, - "required": ["answers"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.reply({\n ...\n})" - } - ] + "description": "Retrieve the current snapshot of available models ordered by release date. The snapshot may precede initial plugin settlement.", + "summary": "List models" } }, - "/question/{requestID}/reject": { - "post": { - "tags": ["question"], - "operationId": "question.reject", + "/api/model/default": { + "get": { + "tags": ["model"], + "operationId": "v2.model.default", "parameters": [ { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^que" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Question rejected successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Question rejected successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", + "description": "Success", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" }, - { - "$ref": "#/components/schemas/InvalidRequestError" + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Info" + }, + { + "type": "null" + } + ] } - ] + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, - "404": { - "description": "QuestionNotFoundError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionNotFoundError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Reject a question request from the AI assistant.", - "summary": "Reject question request", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.reject({\n ...\n})" - } - ] - } - }, - "/permission": { - "get": { - "tags": ["permission"], - "operationId": "permission.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "List of pending permissions", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionRequest" - }, - "description": "List of pending permissions" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "Bad request", + "503": { + "description": "ServiceUnavailableError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/ServiceUnavailableError" } } } } }, - "description": "Get all pending permission requests across all sessions.", - "summary": "List pending permissions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.list({\n ...\n})" - } - ] + "description": "Retrieve the model used when a session has no explicit model selection.", + "summary": "Get default model" } }, - "/permission/{requestID}/reply": { + "/api/generate": { "post": { - "tags": ["permission"], - "operationId": "permission.reply", + "tags": ["generate"], + "operationId": "v2.generate.text", "parameters": [ { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^per" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Permission processed successfully", + "description": "GenerateTextResponse", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Permission processed successfully" + "$ref": "#/components/schemas/GenerateTextResponse" } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/InvalidRequestError1" }, { "$ref": "#/components/schemas/InvalidRequestError" @@ -4926,634 +4550,548 @@ } } }, - "404": { - "description": "PermissionNotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PermissionNotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" } } } } }, - "description": "Approve or deny a permission request from the AI assistant.", - "summary": "Respond to permission request", + "description": "Run one stateless model generation at the requested location and return the assistant text. Uses the location's default model when none is specified.", + "summary": "Generate text", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "reply": { - "type": "string", - "enum": ["once", "always", "reject"] - }, - "message": { + "prompt": { "type": "string" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] } }, - "required": ["reply"], + "required": ["prompt"], "additionalProperties": false } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.reply({\n ...\n})" - } - ] + }, + "required": true + } } }, - "/provider": { + "/api/provider": { "get": { "tags": ["provider"], - "operationId": "provider.list", + "operationId": "v2.provider.list", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "List of providers", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { - "all": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Provider" - } - }, - "default": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "location": { + "$ref": "#/components/schemas/Location.Info" }, - "connected": { + "data": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/Provider.Info" } } }, - "required": ["all", "default", "connected"], - "additionalProperties": false, - "description": "List of providers" + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/ServiceUnavailableError" } } } } }, - "description": "Get a list of all available AI providers, including both available and connected ones.", - "summary": "List providers", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.list({\n ...\n})" - } - ] + "description": "Retrieve active AI providers so clients can show provider availability and configuration.", + "summary": "List providers" } }, - "/provider/auth": { + "/api/provider/{providerID}": { "get": { "tags": ["provider"], - "operationId": "provider.auth", + "operationId": "v2.provider.get", "parameters": [ { - "name": "directory", - "in": "query", + "name": "providerID", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Provider auth methods", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProviderAuthMethod" + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Provider.Info" } }, - "description": "Provider auth methods" + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Retrieve available authentication methods for all AI providers.", - "summary": "Get provider auth methods", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.auth({\n ...\n})" - } - ] - } - }, - "/provider/{providerID}/oauth/authorize": { - "post": { - "tags": ["provider"], - "operationId": "provider.oauth.authorize", - "parameters": [ - { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Authorization URL and method", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProviderAuthAuthorization" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "ProviderAuthError | InvalidRequestError", + "404": { + "description": "ProviderNotFoundError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError1" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/ProviderNotFoundError" } } } - } - }, - "description": "Start the OAuth authorization flow for a provider.", - "summary": "Start OAuth authorization", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "method": { - "type": "number", - "description": "Auth method index" - }, - "inputs": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["method"], - "additionalProperties": false + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } } } } }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.oauth.authorize({\n ...\n})" - } - ] + "description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", + "summary": "Get provider" } }, - "/provider/{providerID}/oauth/callback": { - "post": { - "tags": ["provider"], - "operationId": "provider.oauth.callback", + "/api/integration": { + "get": { + "tags": ["integration"], + "operationId": "v2.integration.list", "parameters": [ { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "OAuth callback processed successfully", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "OAuth callback processed successfully" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Info" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "ProviderAuthError | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError1" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Handle the OAuth callback from a provider after user authorization.", - "summary": "Handle OAuth callback", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "method": { - "type": "number", - "description": "Auth method index" - }, - "code": { - "type": "string" - } - }, - "required": ["method"], - "additionalProperties": false + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } } } }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.oauth.callback({\n ...\n})" - } - ] + "description": "Retrieve available integrations and their authentication methods.", + "summary": "List integrations" } }, - "/session": { + "/api/integration/{integrationID}": { "get": { - "tags": ["session"], - "operationId": "session.list", + "tags": ["integration"], + "operationId": "v2.integration.get", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "scope", - "in": "query", - "schema": { - "type": "string", - "enum": ["project"] - }, - "required": false - }, - { - "name": "path", - "in": "query", + "name": "integrationID", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "roots", + "name": "location", "in": "query", "schema": { "anyOf": [ { - "type": "boolean" + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, { - "type": "string", - "enum": ["true", "false"] + "type": "null" } ] }, - "required": false - }, - { - "name": "start", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "number" - }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "List of sessions", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.Info" + }, + { + "type": "null" + } + ] + } }, - "description": "List of sessions" + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Get a list of all OpenCode sessions, sorted by most recently updated.", - "summary": "List sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["session"], - "operationId": "session.create", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully created session", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Create a new OpenCode session for interacting with AI assistants and managing conversations.", - "summary": "Create session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "title": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["id", "providerID"], - "additionalProperties": false - }, - "metadata": { - "type": "object" - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.create({\n ...\n})" - } - ] + "description": "Retrieve one integration and its authentication methods.", + "summary": "Get integration" } }, - "/session/status": { - "get": { - "tags": ["session"], - "operationId": "session.status", + "/api/experimental/integration/wellknown": { + "post": { + "tags": ["integration"], + "operationId": "v2.experimental.integration.wellknown.add", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "200": { - "description": "Get session status", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/SessionStatus" - }, - "description": "Get session status" - } - } - } + "204": { + "description": "" }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/InvalidRequestError1" }, { "$ref": "#/components/schemas/InvalidRequestError" @@ -5562,68 +5100,106 @@ } } } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } } }, - "description": "Retrieve the current status of all sessions, including active, idle, and completed states.", - "summary": "Get session status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.status({\n ...\n})" - } - ] + "description": "Discover and persist an experimental wellknown integration source.", + "summary": "Add wellknown integration", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": ["url"], + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/session/{sessionID}": { - "get": { - "tags": ["session"], - "operationId": "session.get", + "/api/integration/{integrationID}/connect/key": { + "post": { + "tags": ["integration"], + "operationId": "v2.integration.connect.key", "parameters": [ { - "name": "sessionID", + "name": "integrationID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "200": { - "description": "Get session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } + "204": { + "description": "" }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/InvalidRequestError1" }, { "$ref": "#/components/schemas/InvalidRequestError" @@ -5633,154 +5209,142 @@ } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Retrieve detailed information about a specific OpenCode session.", - "summary": "Get session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.get({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["session"], - "operationId": "session.delete", + "description": "Run a key authentication method and store the resulting credential.", + "summary": "Connect with key", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "answer": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.Answer" + }, + { + "type": "null" + } + ] + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["key"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/oauth": { + "post": { + "tags": ["integration"], + "operationId": "v2.integration.oauth.connect", "parameters": [ { - "name": "sessionID", + "name": "integrationID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully deleted session", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Successfully deleted session" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - { - "$ref": "#/components/schemas/InvalidRequestError" + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } - ] - } - } - } - }, - "404": { - "description": "NotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" + }, + "additionalProperties": false + }, + { + "type": "null" } - } - } - } - }, - "description": "Delete a session and permanently remove all associated data, including messages and history.", - "summary": "Delete session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.delete({\n ...\n})" - } - ] - }, - "patch": { - "tags": ["session"], - "operationId": "session.update", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Successfully updated session", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Session" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Integration.Attempt" + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/InvalidRequestError1" }, { "$ref": "#/components/schemas/InvalidRequestError" @@ -5790,386 +5354,463 @@ } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Update properties of an existing session, such as title or other metadata.", - "summary": "Update session", + "description": "Start an OAuth attempt and return the authorization details.", + "summary": "Begin OAuth connection", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "title": { + "methodID": { "type": "string" }, - "metadata": { - "type": "object" - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" + "answer": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.Answer" + }, + { + "type": "null" + } + ] }, - "time": { - "type": "object", - "properties": { - "archived": { - "type": "number" + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - }, - "additionalProperties": false + ] } }, + "required": ["methodID"], "additionalProperties": false } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.update({\n ...\n})" - } - ] + }, + "required": true + } } }, - "/session/{sessionID}/children": { + "/api/integration/{integrationID}/connect/oauth/{attemptID}": { "get": { - "tags": ["session"], - "operationId": "session.children", + "tags": ["integration"], + "operationId": "v2.integration.oauth.status", "parameters": [ { - "name": "sessionID", + "name": "integrationID", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" + "type": "string" }, "required": true }, { - "name": "directory", - "in": "query", + "name": "attemptID", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "List of children", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Integration.AttemptStatus" + } }, - "description": "List of children" + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Retrieve all child sessions that were forked from the specified parent session.", - "summary": "Get session children", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.children({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/todo": { - "get": { - "tags": ["session"], - "operationId": "session.todo", + "description": "Poll the current status of an OAuth attempt.", + "summary": "Get OAuth attempt status" + }, + "delete": { + "tags": ["integration"], + "operationId": "v2.integration.oauth.cancel", "parameters": [ { - "name": "sessionID", + "name": "integrationID", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" + "type": "string" }, "required": true }, { - "name": "directory", - "in": "query", + "name": "attemptID", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "200": { - "description": "Todo list", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - }, - "description": "Todo list" - } - } - } + "204": { + "description": "" }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Retrieve the todo list associated with a specific session, showing tasks and action items.", - "summary": "Get session todos", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.todo({\n ...\n})" - } - ] + "description": "Cancel an OAuth attempt and release its resources.", + "summary": "Cancel OAuth connection" } }, - "/session/{sessionID}/diff": { - "get": { - "tags": ["session"], - "operationId": "session.diff", + "/api/integration/{integrationID}/connect/oauth/{attemptID}/complete": { + "post": { + "tags": ["integration"], + "operationId": "v2.integration.oauth.complete", "parameters": [ { - "name": "sessionID", + "name": "integrationID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", - "in": "query", + "name": "attemptID", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "messageID", + "name": "location", "in": "query", "schema": { - "type": "string", - "pattern": "^msg" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "200": { - "description": "Successfully retrieved diff", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - }, - "description": "Successfully retrieved diff" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Get the file changes (diff) that resulted from a specific user message in the session.", - "summary": "Get message diff", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.diff({\n ...\n})" - } - ] + "description": "Complete a code-based OAuth attempt and store the resulting credential.", + "summary": "Complete OAuth connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/session/{sessionID}/message": { - "get": { - "tags": ["session"], - "operationId": "session.messages", + "/api/integration/{integrationID}/connect/command": { + "post": { + "tags": ["integration"], + "operationId": "v2.integration.command.connect", "parameters": [ { - "name": "sessionID", + "name": "integrationID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", "schema": { "type": "string" }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "required": false + "required": true }, { - "name": "before", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "List of messages", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Message" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" }, - "required": ["info", "parts"], - "additionalProperties": false + "data": { + "$ref": "#/components/schemas/Integration.CommandAttempt" + } }, - "description": "List of messages" + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/InvalidRequestError1" }, { "$ref": "#/components/schemas/InvalidRequestError" @@ -6179,1413 +5820,1439 @@ } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Retrieve all messages in a session, including user prompts and AI responses.", - "summary": "Get session messages", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.messages({\n ...\n})" - } - ] - }, - "post": { - "tags": ["session"], - "operationId": "session.prompt", + "description": "Start a command authentication attempt.", + "summary": "Begin command connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["methodID"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/command/{attemptID}": { + "get": { + "tags": ["integration"], + "operationId": "v2.integration.command.status", "parameters": [ { - "name": "sessionID", + "name": "integrationID", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" + "type": "string" }, "required": true }, { - "name": "directory", - "in": "query", + "name": "attemptID", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Created message", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", - "required": ["info", "parts"], "properties": { - "info": { - "$ref": "#/components/schemas/AssistantMessage" + "location": { + "$ref": "#/components/schemas/Location.Info" }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } + "data": { + "$ref": "#/components/schemas/Integration.CommandAttemptStatus" } - } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Create and send a new message to a session, streaming the AI response.", - "summary": "Send message", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "agent": { - "type": "string" - }, - "noReply": { - "type": "boolean" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - }, - "format": { - "$ref": "#/components/schemas/OutputFormat" - }, - "system": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { + "description": "Poll the current status and output of a command authentication attempt.", + "summary": "Get command attempt status" + }, + "delete": { + "tags": ["integration"], + "operationId": "v2.integration.command.cancel", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { "anyOf": [ { - "$ref": "#/components/schemas/TextPartInput" + "type": "string" }, { - "$ref": "#/components/schemas/FilePartInput" - }, + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ { - "$ref": "#/components/schemas/AgentPartInput" + "type": "string" }, { - "$ref": "#/components/schemas/SubtaskPartInput" + "type": "null" } ] } - } + }, + "additionalProperties": false }, - "required": ["parts"], - "additionalProperties": false + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } } } }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.prompt({\n ...\n})" - } - ] + "description": "Cancel a command authentication attempt and terminate its process.", + "summary": "Cancel command connection" } }, - "/session/{sessionID}/message/{messageID}": { + "/api/mcp": { "get": { - "tags": ["session"], - "operationId": "session.message", + "tags": ["mcp"], + "operationId": "v2.mcp.list", "parameters": [ { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Message", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { - "info": { - "$ref": "#/components/schemas/Message" + "location": { + "$ref": "#/components/schemas/Location.Info" }, - "parts": { + "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/Mcp.Server" } } }, - "required": ["info", "parts"], - "additionalProperties": false, - "description": "Message" + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Retrieve a specific message from a session by its message ID.", - "summary": "Get message", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.message({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["session"], - "operationId": "session.deleteMessage", + "description": "Retrieve configured MCP servers and their connection status.", + "summary": "List MCP servers" + } + }, + "/api/mcp/{server}": { + "put": { + "tags": ["mcp"], + "operationId": "v2.mcp.add", "parameters": [ { - "name": "sessionID", + "name": "server", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" + "type": "string" }, "required": true }, { - "name": "messageID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "pattern": "^msg" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Add an MCP server at runtime or replace an existing one, connecting it immediately.", + "summary": "Add MCP server", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.LocalConfig" + }, + { + "$ref": "#/components/schemas/Mcp.RemoteConfig" + } + ] + } + }, + "required": ["config"], + "additionalProperties": false + } + } }, + "required": true + } + }, + "delete": { + "tags": ["mcp"], + "operationId": "v2.mcp.remove", + "parameters": [ { - "name": "directory", - "in": "query", + "name": "server", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "200": { - "description": "Successfully deleted message", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Successfully deleted message" - } - } - } + "204": { + "description": "" }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "409": { - "description": "SessionBusyError", + "404": { + "description": "McpServerNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionBusyError" + "$ref": "#/components/schemas/McpServerNotFoundError" } } } } }, - "description": "Permanently delete a specific message and all of its parts from a session without reverting file changes.", - "summary": "Delete message", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.deleteMessage({\n ...\n})" - } - ] + "description": "Stop an MCP server and remove it from the runtime set until restart.", + "summary": "Remove MCP server" } }, - "/session/{sessionID}/fork": { + "/api/mcp/{server}/connect": { "post": { - "tags": ["session"], - "operationId": "session.fork", + "tags": ["mcp"], + "operationId": "v2.mcp.connect", "parameters": [ { - "name": "sessionID", + "name": "server", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "200": { - "description": "200", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "NotFoundError", + "description": "McpServerNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/McpServerNotFoundError" } } } } }, - "description": "Create a new session by forking an existing session at a specific message point.", - "summary": "Fork session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.fork({\n ...\n})" - } - ] + "description": "Connect an MCP server at runtime, overriding a disabled configuration until restart.", + "summary": "Connect MCP server" } }, - "/session/{sessionID}/abort": { + "/api/mcp/{server}/disconnect": { "post": { - "tags": ["session"], - "operationId": "session.abort", + "tags": ["mcp"], + "operationId": "v2.mcp.disconnect", "parameters": [ { - "name": "sessionID", + "name": "server", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" + "type": "string" }, "required": true }, { - "name": "directory", + "name": "location", "in": "query", "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "200": { - "description": "Aborted session", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Aborted session" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "McpServerNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/McpServerNotFoundError" } } } } }, - "description": "Abort an active session and stop any ongoing AI processing or command execution.", - "summary": "Abort session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.abort({\n ...\n})" - } - ] + "description": "Disconnect an MCP server at runtime, removing its tools until reconnected.", + "summary": "Disconnect MCP server" } }, - "/session/{sessionID}/init": { - "post": { - "tags": ["session"], - "operationId": "session.init", + "/api/mcp/resource": { + "get": { + "tags": ["mcp"], + "operationId": "v2.mcp.resource.catalog", "parameters": [ { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "200", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "200" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Mcp.ResourceCatalog" + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.", - "summary": "Initialize session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["modelID", "providerID", "messageID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.init({\n ...\n})" - } - ] + "description": "Retrieve resources and resource templates from connected MCP servers.", + "summary": "List MCP resources" } }, - "/session/{sessionID}/share": { - "post": { - "tags": ["session"], - "operationId": "session.share", + "/api/credential/{credentialID}": { + "patch": { + "tags": ["credential"], + "operationId": "v2.credential.update", "parameters": [ { - "name": "sessionID", + "name": "credentialID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "200": { - "description": "Successfully shared session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } + "204": { + "description": "" }, "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "NotFoundError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "500": { - "description": "InternalServerError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_InternalServerError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Create a shareable link for a session, allowing others to view the conversation.", - "summary": "Share session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.share({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["session"], - "operationId": "session.unshare", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true + "description": "Update a stored credential label.", + "summary": "Update credential", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + }, + "required": ["label"], + "additionalProperties": false + } + } }, + "required": true + } + }, + "delete": { + "tags": ["credential"], + "operationId": "v2.credential.remove", + "parameters": [ { - "name": "directory", - "in": "query", + "name": "credentialID", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "200": { - "description": "Successfully unshared session", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Remove a stored integration credential.", + "summary": "Remove credential" + } + }, + "/api/project": { + "get": { + "tags": ["project"], + "operationId": "v2.project.list", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } } } } }, - "404": { - "description": "NotFoundError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "500": { - "description": "InternalServerError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_InternalServerError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Remove the shareable link for a session, making it private again.", - "summary": "Unshare session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.unshare({\n ...\n})" - } - ] + "description": "List known projects.", + "summary": "List projects" } }, - "/session/{sessionID}/summarize": { - "post": { - "tags": ["session"], - "operationId": "session.summarize", + "/api/project/current": { + "get": { + "tags": ["project"], + "operationId": "v2.project.current", "parameters": [ { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Summarized session", + "description": "Project.Current", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Summarized session" + "$ref": "#/components/schemas/Project.Current" } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Generate a concise summary of the session using AI compaction to preserve key information.", - "summary": "Summarize session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - }, - "auto": { - "type": "boolean" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.summarize({\n ...\n})" - } - ] + "description": "Resolve the project for the requested location.", + "summary": "Get current project" } }, - "/session/{sessionID}/prompt_async": { - "post": { - "tags": ["session"], - "operationId": "session.prompt_async", + "/api/project/{projectID}/directories": { + "get": { + "tags": ["project"], + "operationId": "v2.project.directories", "parameters": [ { - "name": "sessionID", + "name": "projectID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "204": { - "description": "Prompt accepted" - }, - "400": { - "description": "BadRequest | InvalidRequestError", + "200": { + "description": "Project.Directories", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/Project.Directories" } } } }, - "404": { - "description": "NotFoundError", + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.", - "summary": "Send async message", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "agent": { - "type": "string" - }, - "noReply": { - "type": "boolean" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - }, - "format": { - "$ref": "#/components/schemas/OutputFormat" - }, - "system": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { + "description": "List known local absolute directories for a project.", + "summary": "List project directories" + } + }, + "/api/form/request": { + "get": { + "tags": ["form"], + "operationId": "v2.form.request.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { "anyOf": [ { - "$ref": "#/components/schemas/TextPartInput" + "type": "string" }, { - "$ref": "#/components/schemas/FilePartInput" - }, + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ { - "$ref": "#/components/schemas/AgentPartInput" + "type": "string" }, { - "$ref": "#/components/schemas/SubtaskPartInput" + "type": "null" } ] } - } + }, + "additionalProperties": false }, - "required": ["parts"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.prompt_async({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/command": { - "post": { - "tags": ["session"], - "operationId": "session.command", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Created message", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", - "required": ["info", "parts"], "properties": { - "info": { - "$ref": "#/components/schemas/AssistantMessage" + "location": { + "$ref": "#/components/schemas/Location.Info" }, - "parts": { + "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/Form.Info" } } - } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Send a new command to a session for execution by the AI assistant.", - "summary": "Send command", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "arguments": { - "type": "string" - }, - "command": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["file"] - }, - "mime": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/FilePartSource" - } - }, - "required": ["type", "mime", "url"], - "additionalProperties": false - } - } - }, - "required": ["arguments", "command"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.command({\n ...\n})" - } - ] + "description": "Retrieve pending forms for a location.", + "summary": "List pending form requests" } }, - "/session/{sessionID}/shell": { - "post": { - "tags": ["session"], - "operationId": "session.shell", + "/api/session/{sessionID}/form": { + "get": { + "tags": ["form"], + "operationId": "v2.session.form.list", "parameters": [ { "name": "sessionID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", "schema": { "type": "string" }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "Created message", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { - "info": { - "$ref": "#/components/schemas/Message" - }, - "parts": { + "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/Form.Info" } } }, - "required": ["info", "parts"], - "additionalProperties": false, - "description": "Created message" + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "409": { - "description": "SessionBusyError", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionBusyError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Execute a shell command within the session context and return the AI's response.", - "summary": "Run shell command", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "command": { - "type": "string" - } - }, - "required": ["agent", "command"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.shell({\n ...\n})" - } - ] - } - }, - "/session/{sessionID}/revert": { + "description": "Retrieve pending forms for a session.", + "summary": "List session forms" + }, "post": { - "tags": ["session"], - "operationId": "session.revert", + "tags": ["form"], + "operationId": "v2.session.form.create", "parameters": [ { "name": "sessionID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", "schema": { "type": "string" }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "Updated session", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Session" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Info" + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/InvalidRequestError1" }, { "$ref": "#/components/schemas/InvalidRequestError" @@ -7595,228 +7262,229 @@ } } }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, "404": { - "description": "NotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } }, "409": { - "description": "SessionBusyError", + "description": "ConflictError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionBusyError" + "$ref": "#/components/schemas/ConflictError" } } } } }, - "description": "Revert a specific message in a session, undoing its effects and restoring the previous state.", - "summary": "Revert message", + "description": "Create a form for a session.", + "summary": "Create session form", "requestBody": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } - }, - "required": ["messageID"], - "additionalProperties": false + "$ref": "#/components/schemas/Form.CreatePayload" } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.revert({\n ...\n})" - } - ] + }, + "required": true + } } }, - "/session/{sessionID}/unrevert": { - "post": { - "tags": ["session"], - "operationId": "session.unrevert", + "/api/session/{sessionID}/form/{formID}": { + "get": { + "tags": ["form"], + "operationId": "v2.session.form.get", "parameters": [ { "name": "sessionID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "directory", - "in": "query", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "workspace", - "in": "query", + "name": "formID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "Updated session", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Session" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Info" + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "409": { - "description": "SessionBusyError", + "404": { + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionBusyError" + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Restore all previously reverted messages in a session.", - "summary": "Restore reverted messages", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.unrevert({\n ...\n})" - } - ] + "description": "Retrieve a form for a session.", + "summary": "Get session form" } }, - "/session/{sessionID}/permissions/{permissionID}": { - "post": { - "tags": ["session"], - "operationId": "permission.respond", + "/api/session/{sessionID}/form/{formID}/state": { + "get": { + "tags": ["form"], + "operationId": "v2.session.form.state", "parameters": [ { "name": "sessionID", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" + "type": "string" }, "required": true }, { - "name": "permissionID", + "name": "formID", "in": "path", "schema": { "type": "string", - "pattern": "^per" + "allOf": [ + { + "pattern": "^frm_" + } + ] }, "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false } ], + "security": [], "responses": { "200": { - "description": "Permission processed successfully", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Permission processed successfully" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.State" + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "NotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/FormNotFoundError" }, { - "$ref": "#/components/schemas/PermissionNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -7824,635 +7492,493 @@ } } }, - "description": "Approve or deny a permission request from the AI assistant.", - "summary": "Respond to permission", - "deprecated": true, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "response": { - "type": "string", - "enum": ["once", "always", "reject"] - } - }, - "required": ["response"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.respond({\n ...\n})" - } - ] + "description": "Retrieve the current state for a form.", + "summary": "Get form state" } }, - "/session/{sessionID}/message/{messageID}/part/{partID}": { - "delete": { - "tags": ["session"], - "operationId": "part.delete", + "/api/session/{sessionID}/form/{formID}/reply": { + "post": { + "tags": ["form"], + "operationId": "v2.session.form.reply", "parameters": [ { "name": "sessionID", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg" + "type": "string" }, "required": true }, { - "name": "partID", + "name": "formID", "in": "path", "schema": { "type": "string", - "pattern": "^prt" + "allOf": [ + { + "pattern": "^frm_" + } + ] }, "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false + "400": { + "description": "FormInvalidAnswerError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormInvalidAnswerError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Successfully deleted part", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Successfully deleted part" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "404": { + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/FormNotFoundError" }, { - "$ref": "#/components/schemas/InvalidRequestError" + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" } ] } } } }, - "404": { - "description": "NotFoundError", + "409": { + "description": "FormAlreadySettledError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/FormAlreadySettledError" } } } } }, - "description": "Delete a part from a message.", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.delete({\n ...\n})" - } - ] - }, - "patch": { - "tags": ["session"], - "operationId": "part.update", + "description": "Submit an answer to a pending form.", + "summary": "Reply to form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}/cancel": { + "post": { + "tags": ["form"], + "operationId": "v2.session.form.cancel", "parameters": [ { "name": "sessionID", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg" + "type": "string" }, "required": true }, { - "name": "partID", + "name": "formID", "in": "path", "schema": { "type": "string", - "pattern": "^prt" + "allOf": [ + { + "pattern": "^frm_" + } + ] }, "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false } ], + "security": [], "responses": { - "200": { - "description": "Successfully updated part", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/FormNotFoundError" }, { - "$ref": "#/components/schemas/InvalidRequestError" + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" } ] } } } }, - "404": { - "description": "NotFoundError", + "409": { + "description": "FormAlreadySettledError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/FormAlreadySettledError" } } } } }, - "description": "Update a part in a message.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Part" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.part.update({\n ...\n})" - } - ] + "description": "Cancel a pending form.", + "summary": "Cancel form" } }, - "/sync/start": { - "post": { - "tags": ["sync"], - "operationId": "sync.start", + "/api/permission/request": { + "get": { + "tags": ["permission"], + "operationId": "v2.permission.request.list", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Workspace sync started", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Workspace sync started" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Permission.Request" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Start sync loops for workspaces in the current project that have active sessions.", - "summary": "Start workspace sync", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.start({\n ...\n})" - } - ] + "description": "Retrieve pending permission requests for a location.", + "summary": "List pending permission requests" } }, - "/sync/replay": { - "post": { - "tags": ["sync"], - "operationId": "sync.replay", + "/api/permission/saved": { + "get": { + "tags": ["permission"], + "operationId": "v2.permission.saved.list", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "projectID", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "required": false } ], + "security": [], "responses": { "200": { - "description": "Replayed sync events", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { - "sessionID": { - "type": "string" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionSaved.Info" + } } }, - "required": ["sessionID"], - "additionalProperties": false, - "description": "Replayed sync events" + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Validate and replay a complete sync event history.", - "summary": "Replay sync events", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "events": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "minimum": 0 - }, - "type": { - "type": "string" - }, - "data": { - "type": "object" - } - }, - "required": ["id", "aggregateID", "seq", "type", "data"], - "additionalProperties": false - } - } - }, - "required": ["directory", "events"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.replay({\n ...\n})" - } - ] + "description": "Retrieve saved permissions, optionally filtered by project.", + "summary": "List saved permissions" } }, - "/sync/steal": { - "post": { - "tags": ["sync"], - "operationId": "sync.steal", + "/api/permission/saved/{id}": { + "delete": { + "tags": ["permission"], + "operationId": "v2.permission.saved.remove", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "id", + "in": "path", "schema": { "type": "string" }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "Session stolen into workspace", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false, - "description": "Session stolen into workspace" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Update a session to belong to the current workspace through the sync event system.", - "summary": "Steal session into workspace", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.steal({\n ...\n})" - } - ] + "description": "Remove a saved permission by ID.", + "summary": "Remove saved permission" } }, - "/sync/history": { + "/api/session/{sessionID}/permission": { "post": { - "tags": ["sync"], - "operationId": "sync.history.list", + "tags": ["permission"], + "operationId": "v2.session.permission.create", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "Sync events", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "aggregate_id": { - "type": "string" - }, - "seq": { - "type": "integer", - "minimum": 0 - }, - "type": { - "type": "string" + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "effect": { + "$ref": "#/components/schemas/Permission.Effect" + } }, - "data": { - "type": "object" - } - }, - "required": ["id", "aggregate_id", "seq", "type", "data"], - "additionalProperties": false + "required": ["id", "effect"], + "additionalProperties": false + } }, - "description": "Sync events" + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "description": "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.", - "summary": "List sync events", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "type": "integer", - "minimum": 0 + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.history.list({\n ...\n})" - } - ] - } - }, - "/tui/append-prompt": { - "post": { - "tags": ["tui"], - "operationId": "tui.appendPrompt", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Prompt processed successfully", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Prompt processed successfully" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "BadRequest | InvalidRequestError", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" + "$ref": "#/components/schemas/SessionNotFoundError" }, { - "$ref": "#/components/schemas/InvalidRequestError" + "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -8460,1328 +7986,1388 @@ } } }, - "description": "Append prompt to the TUI.", - "summary": "Append TUI prompt", + "description": "Evaluate and, when approval is required, create a permission request for a session.", + "summary": "Create permission request", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "text": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + { + "type": "null" + } + ] + }, + "action": { "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/Permission.Source" + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, - "required": ["text"], + "required": ["action", "resources"], "additionalProperties": false } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.appendPrompt({\n ...\n})" - } - ] - } - }, - "/tui/open-help": { - "post": { - "tags": ["tui"], - "operationId": "tui.openHelp", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, + "required": true + } + }, + "get": { + "tags": ["permission"], + "operationId": "v2.session.permission.list", + "parameters": [ { - "name": "workspace", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "Help dialog opened successfully", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Help dialog opened successfully" + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Permission.Request" + } + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Open the help dialog in the TUI to display user assistance information.", - "summary": "Open help dialog", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openHelp({\n ...\n})" - } - ] - } - }, - "/tui/open-sessions": { - "post": { - "tags": ["tui"], - "operationId": "tui.openSessions", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Session dialog opened successfully", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Session dialog opened successfully" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "Bad request", + "404": { + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Open the session dialog.", - "summary": "Open sessions dialog", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openSessions({\n ...\n})" - } - ] + "description": "Retrieve pending permission requests owned by a session.", + "summary": "List session permission requests" } }, - "/tui/open-themes": { - "post": { - "tags": ["tui"], - "operationId": "tui.openThemes", + "/api/session/{sessionID}/permission/{requestID}": { + "get": { + "tags": ["permission"], + "operationId": "v2.session.permission.get", "parameters": [ { - "name": "directory", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true }, { - "name": "workspace", - "in": "query", + "name": "requestID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { "200": { - "description": "Theme dialog opened successfully", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Theme dialog opened successfully" + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Permission.Request" + } + }, + "required": ["data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Open the theme dialog.", - "summary": "Open themes dialog", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openThemes({\n ...\n})" - } - ] - } - }, - "/tui/open-models": { - "post": { - "tags": ["tui"], - "operationId": "tui.openModels", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Model dialog opened successfully", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Model dialog opened successfully" + "$ref": "#/components/schemas/UnauthorizedError" } } } }, - "400": { - "description": "Bad request", + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Open the model dialog.", - "summary": "Open models dialog", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openModels({\n ...\n})" - } - ] + "description": "Retrieve a pending permission request owned by a session.", + "summary": "Get permission request" } }, - "/tui/submit-prompt": { + "/api/session/{sessionID}/permission/{requestID}/reply": { "post": { - "tags": ["tui"], - "operationId": "tui.submitPrompt", + "tags": ["permission"], + "operationId": "v2.session.permission.reply", "parameters": [ { - "name": "directory", - "in": "query", + "name": "sessionID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "required": false + "required": true }, { - "name": "workspace", - "in": "query", + "name": "requestID", + "in": "path", "schema": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] }, - "required": false + "required": true } ], + "security": [], "responses": { - "200": { - "description": "Prompt submitted successfully", + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Prompt submitted successfully" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } - } - }, - "description": "Submit the prompt.", - "summary": "Submit TUI prompt", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.submitPrompt({\n ...\n})" - } - ] - } - }, - "/tui/clear-prompt": { - "post": { - "tags": ["tui"], - "operationId": "tui.clearPrompt", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Prompt cleared successfully", + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Prompt cleared successfully" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Clear the prompt.", - "summary": "Clear TUI prompt", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.clearPrompt({\n ...\n})" - } - ] - } - }, - "/tui/execute-command": { - "post": { - "tags": ["tui"], - "operationId": "tui.executeCommand", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Command executed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Command executed successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } } }, - "description": "Execute a TUI command.", - "summary": "Execute TUI command", + "description": "Respond to a pending permission request owned by a session.", + "summary": "Reply to pending permission request", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "command": { - "type": "string" + "reply": { + "$ref": "#/components/schemas/Permission.Reply" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, - "required": ["command"], + "required": ["reply"], "additionalProperties": false } } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.executeCommand({\n ...\n})" - } - ] + }, + "required": true + } } }, - "/tui/show-toast": { - "post": { - "tags": ["tui"], - "operationId": "tui.showToast", + "/api/fs/read/*": { + "get": { + "tags": ["filesystem"], + "operationId": "v2.fs.read", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Toast notification shown successfully", + "description": "Success", "content": { - "application/json": { + "application/octet-stream": { "schema": { - "type": "boolean", - "description": "Toast notification shown successfully" + "type": "string", + "format": "binary" } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Show a toast notification in the TUI.", - "summary": "Show TUI toast", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } } } } }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.showToast({\n ...\n})" - } - ] + "description": "Serve one file relative to the requested location.", + "summary": "Read file" } }, - "/tui/publish": { - "post": { - "tags": ["tui"], - "operationId": "tui.publish", + "/api/fs/list": { + "get": { + "tags": ["filesystem"], + "operationId": "v2.fs.list", "parameters": [ { - "name": "directory", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true }, { - "name": "workspace", + "name": "path", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "required": false } ], + "security": [], "responses": { "200": { - "description": "Event published successfully", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Event published successfully" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Publish a TUI event.", - "summary": "Publish TUI event", - "requestBody": { - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/EventTuiPromptAppend" - }, - { - "$ref": "#/components/schemas/EventTuiCommandExecute" - }, - { - "$ref": "#/components/schemas/EventTuiToastShow" - }, - { - "$ref": "#/components/schemas/EventTuiSessionSelect" - } - ] - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.publish({\n ...\n})" - } - ] + "description": "List direct children of one directory relative to the requested location.", + "summary": "List directory" } }, - "/tui/select-session": { - "post": { - "tags": ["tui"], - "operationId": "tui.selectSession", + "/api/fs/find": { + "get": { + "tags": ["filesystem"], + "operationId": "v2.fs.find", "parameters": [ { - "name": "directory", + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "query", "in": "query", "schema": { "type": "string" }, + "required": true + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": ["file", "directory"] + }, "required": false }, { - "name": "workspace", + "name": "limit", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "required": false } ], + "security": [], "responses": { "200": { - "description": "Session selected successfully", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Session selected successfully" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "404": { - "description": "NotFoundError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Navigate the TUI to display the specified session.", - "summary": "Select session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.selectSession({\n ...\n})" - } - ] + "description": "Find recursively ranked filesystem entries relative to the requested location.", + "summary": "Find files" } }, - "/tui/control/next": { + "/api/command": { "get": { - "tags": ["tui"], - "operationId": "tui.control.next", + "tags": ["command"], + "operationId": "v2.command.list", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Next TUI request", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { - "path": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Info" }, - "body": {} + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Command.Info" + } + } }, - "required": ["path", "body"], - "additionalProperties": false, - "description": "Next TUI request" + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Retrieve the next TUI request from the queue for processing.", - "summary": "Get next TUI request", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.control.next({\n ...\n})" - } - ] + "description": "Retrieve currently registered commands.", + "summary": "List commands" } }, - "/tui/control/response": { - "post": { - "tags": ["tui"], - "operationId": "tui.control.response", + "/api/skill": { + "get": { + "tags": ["skill"], + "operationId": "v2.skill.list", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Response submitted successfully", + "description": "Success", "content": { "application/json": { "schema": { - "type": "boolean", - "description": "Response submitted successfully" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Skill.Info" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Submit a response to the TUI request queue to complete a pending request.", - "summary": "Submit TUI response", - "requestBody": { - "content": { - "application/json": { - "schema": {} + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } } } }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.control.response({\n ...\n})" - } - ] + "description": "Retrieve currently registered skills.", + "summary": "List skills" } }, - "/experimental/workspace/adapter": { + "/api/event": { "get": { - "tags": ["workspace"], - "operationId": "experimental.workspace.adapter.list", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], + "tags": ["event"], + "operationId": "v2.event.subscribe", + "parameters": [], + "security": [], "responses": { "200": { - "description": "Workspace adapters", + "description": "Success", "content": { - "application/json": { + "text/event-stream": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - } + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "required": ["type", "name", "description"], - "additionalProperties": false + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/V2EventJsonString" + } + }, + "required": ["id", "event", "data"], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["Fail"] + }, + "error": { + "not": {} + } + }, + "required": ["_tag", "error"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["Die"] + }, + "defect": {} + }, + "required": ["_tag", "defect"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["Interrupt"] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": ["_tag", "fiberId"], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} }, - "description": "Workspace adapters" + "failureEvent": "effect/httpapi/stream/failure" } } } }, "400": { - "description": "Bad request", + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "List all available workspace adapters for the current project.", - "summary": "List workspace adapters", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adapter.list({\n ...\n})" - } - ] + "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed.", + "summary": "Subscribe to events" } }, - "/experimental/workspace": { + "/api/pty": { "get": { - "tags": ["workspace"], - "operationId": "experimental.workspace.list", + "tags": ["pty"], + "operationId": "v2.pty.list", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Workspaces", + "description": "Success", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Workspace" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pty" + } + } }, - "description": "Workspaces" + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "List all workspaces.", - "summary": "List workspaces", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["workspace"], - "operationId": "experimental.workspace.create", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Workspace created", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Workspace" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "WorkspaceCreateError | BadRequest | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorkspaceCreateError" - }, - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Create a workspace for the current project.", - "summary": "Create workspace", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^wrk" - }, - "type": { - "type": "string" - }, - "branch": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "extra": { - "anyOf": [ - {}, - { - "type": "null" - } - ] - } - }, - "required": ["type"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.create({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/sync-list": { + "description": "List PTY sessions for a location, including exited sessions retained until removal.", + "summary": "List PTY sessions" + }, "post": { - "tags": ["workspace"], - "operationId": "experimental.workspace.syncList", + "tags": ["pty"], + "operationId": "v2.pty.create", "parameters": [ { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { - "204": { - "description": "Workspace list synced" - }, - "400": { - "description": "Bad request", + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } - } - }, - "description": "Register missing workspaces returned by workspace adapters.", - "summary": "Sync workspace list", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.syncList({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/status": { - "get": { - "tags": ["workspace"], - "operationId": "experimental.workspace.status", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Workspace status", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WorkspaceEventConnectionStatus" - }, - "description": "Workspace status" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Get connection status for workspaces in the current project.", - "summary": "Workspace status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.status({\n ...\n})" - } - ] + "description": "Create a pseudo-terminal session for a location.", + "summary": "Create PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "title": { + "type": "string" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/experimental/workspace/{id}": { - "delete": { - "tags": ["workspace"], - "operationId": "experimental.workspace.remove", + "/api/pty/{ptyID}": { + "get": { + "tags": ["pty"], + "operationId": "v2.pty.get", "parameters": [ { - "name": "id", + "name": "ptyID", "in": "path", "schema": { "type": "string", - "pattern": "^wrk" + "allOf": [ + { + "pattern": "^pty" + } + ] }, "required": true }, { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], + "security": [], "responses": { "200": { - "description": "Workspace removed", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Workspace" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "BadRequest | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } - } - }, - "description": "Remove an existing workspace.", - "summary": "Remove workspace", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.remove({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/warp": { - "post": { - "tags": ["workspace"], - "operationId": "experimental.workspace.warp", - "parameters": [ - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "204": { - "description": "Session warped" }, - "400": { - "description": "WorkspaceWarpError | VcsApplyError | InvalidRequestError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorkspaceWarpError" - }, - { - "$ref": "#/components/schemas/VcsApplyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/UnauthorizedError" } } } }, "404": { - "description": "NotFoundError", + "description": "PtyNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/PtyNotFoundError" } } } } }, - "description": "Move a session's sync history into the target workspace, or detach it to the local project.", - "summary": "Warp session into workspace", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "anyOf": [ - { - "type": "string", - "pattern": "^wrk" - }, - { - "type": "null" - } - ] - }, - "sessionID": { - "type": "string", - "pattern": "^ses" + "description": "Get one PTY session, including its exit code once exited.", + "summary": "Get PTY session" + }, + "put": { + "tags": ["pty"], + "operationId": "v2.pty.update", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } }, - "copyChanges": { - "type": "boolean" - } + "additionalProperties": false }, - "required": ["id", "sessionID"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.warp({\n ...\n})" + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - ] - } - }, - "/api/health": { - "get": { - "tags": ["opencode HttpApi"], - "operationId": "v2.health.get", - "parameters": [], + ], "security": [], "responses": { "200": { @@ -9791,12 +9377,14 @@ "schema": { "type": "object", "properties": { - "healthy": { - "type": "boolean", - "enum": [true] + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" } }, - "required": ["healthy"], + "required": ["location", "data"], "additionalProperties": false } } @@ -9821,37 +9409,112 @@ } } } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } } }, - "description": "Check whether the API server is ready to accept requests.", - "summary": "Check server health", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.health.get({\n ...\n})" - } - ] - } - }, - "/api/location": { - "get": { - "tags": ["opencode HttpApi"], - "operationId": "v2.location.get", + "description": "Update the title or viewport size of one PTY session.", + "summary": "Update PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "size": { + "type": "object", + "properties": { + "rows": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "cols": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": ["rows", "cols"], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": ["pty"], + "operationId": "v2.pty.remove", "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, { "name": "location", "in": "query", "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, - "workspace": { - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, "required": false, "style": "deepObject", @@ -9860,15 +9523,8 @@ ], "security": [], "responses": { - "200": { - "description": "Location.Info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LocationInfo" - } - } - } + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -9889,37 +9545,75 @@ } } } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } } }, - "description": "Resolve the requested location or the server default location.", - "summary": "Get location", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.location.get({\n ...\n})" - } - ] + "description": "Terminate and remove one PTY session.", + "summary": "Remove PTY session" } }, - "/api/agent": { - "get": { - "tags": ["opencode HttpApi"], - "operationId": "v2.agent.list", + "/api/pty/{ptyID}/connect-token": { + "post": { + "tags": ["pty"], + "operationId": "v2.pty.connect.token", "parameters": [ { - "name": "location", - "in": "query", + "name": "ptyID", + "in": "path", "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, - "workspace": { - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, "required": false, "style": "deepObject", @@ -9936,13 +9630,10 @@ "type": "object", "properties": { "location": { - "$ref": "#/components/schemas/LocationInfo" + "$ref": "#/components/schemas/Location.Info" }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AgentV2Info" - } + "$ref": "#/components/schemas/PtyTicket.ConnectToken" } }, "required": ["location", "data"], @@ -9970,119 +9661,97 @@ } } } + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } } }, - "description": "Retrieve currently registered agents.", - "summary": "List agents", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.agent.list({\n ...\n})" - } - ] + "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", + "summary": "Create PTY WebSocket token" } }, - "/api/session": { + "/api/pty/{ptyID}/connect": { "get": { - "tags": ["sessions"], - "operationId": "v2.session.list", + "tags": ["pty"], + "operationId": "v2.pty.connect", "parameters": [ { - "name": "workspace", - "in": "query", - "schema": { - "type": "string", - "pattern": "^wrk" - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "order", - "in": "query", + "name": "ptyID", + "in": "path", "schema": { "type": "string", - "enum": ["asc", "desc"] + "allOf": [ + { + "pattern": "^pty" + } + ] }, - "required": false + "required": true }, { - "name": "search", "in": "query", + "name": "location[directory]", "schema": { "type": "string" - }, - "required": false + } }, { - "name": "directory", "in": "query", + "name": "location[workspace]", "schema": { "type": "string" - }, - "required": false + } }, { - "name": "project", "in": "query", + "name": "cursor", "schema": { "type": "string" - }, - "required": false + } }, { - "name": "subpath", "in": "query", + "name": "ticket", "schema": { "type": "string" - }, - "required": false - }, - { - "name": "cursor", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response." - }, - "required": false + } } ], "security": [], "responses": { "200": { - "description": "SessionsResponse", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionsResponse" + "type": "boolean" } } } }, "400": { - "description": "InvalidCursorError | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidCursorError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -10096,21 +9765,79 @@ } } } + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } } }, - "description": "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", - "summary": "List sessions", - "x-codeSamples": [ + "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", + "summary": "Connect to PTY session", + "x-websocket": true + } + }, + "/api/shell": { + "get": { + "tags": ["shell"], + "operationId": "v2.shell.list", + "parameters": [ { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.list({\n ...\n})" + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } - ] - }, - "post": { - "tags": ["sessions"], - "operationId": "v2.session.create", - "parameters": [], + ], "security": [], "responses": { "200": { @@ -10120,11 +9847,17 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { - "$ref": "#/components/schemas/SessionV2Info" + "type": "array", + "items": { + "$ref": "#/components/schemas/Shell.Info1" + } } }, - "required": ["data"], + "required": ["location", "data"], "additionalProperties": false } } @@ -10151,66 +9884,71 @@ } } }, - "description": "Create a session at the requested location.", - "summary": "Create session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^ses" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - } - }, - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ + "description": "List currently running shell commands for a location. Exited commands are not included.", + "summary": "List running shell commands" + }, + "post": { + "tags": ["shell"], + "operationId": "v2.shell.create", + "parameters": [ { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.create({\n ...\n})" - } - ] - } - }, - "/api/session/active": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.active", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { - "type": "object", - "patternProperties": { - "^ses": { - "$ref": "#/components/schemas/SessionActive" - } - } + "$ref": "#/components/schemas/Shell.Info1" } }, - "required": ["data"], + "required": ["location", "data"], "additionalProperties": false } } @@ -10237,29 +9975,98 @@ } } }, - "description": "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive.", - "summary": "List active sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.active({\n ...\n})" - } - ] + "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", + "summary": "Run shell command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "metadata": { + "type": "object" + } + }, + "required": ["command", "timeout"], + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/api/session/{sessionID}": { + "/api/shell/{id}": { "get": { - "tags": ["sessions"], - "operationId": "v2.session.get", + "tags": ["shell"], + "operationId": "v2.shell.get", "parameters": [ { - "name": "sessionID", + "name": "id", "in": "path", "schema": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -10271,11 +10078,14 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { - "$ref": "#/components/schemas/SessionV2Info" + "$ref": "#/components/schemas/Shell.Info1" } }, - "required": ["data"], + "required": ["location", "data"], "additionalProperties": false } } @@ -10302,46 +10112,75 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "ShellNotFoundError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/ShellNotFoundError" } } } } }, - "description": "Retrieve a session by ID.", - "summary": "Get session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.get({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/agent": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.switchAgent", + "description": "Get one shell command, including its status and exit code once exited.", + "summary": "Get shell command" + }, + "delete": { + "tags": ["shell"], + "operationId": "v2.shell.remove", "parameters": [ { - "name": "sessionID", + "name": "id", "in": "path", "schema": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -10370,148 +10209,77 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "ShellNotFoundError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/ShellNotFoundError" } } } } }, - "description": "Switch the agent used by subsequent provider turns.", - "summary": "Switch session agent", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "agent": { - "type": "string" - } - }, - "required": ["agent"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.switchAgent({\n ...\n})" - } - ] + "description": "Terminate and remove one shell command and its retained output.", + "summary": "Remove shell command" } }, - "/api/session/{sessionID}/model": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.switchModel", + "/api/shell/{id}/timeout": { + "patch": { + "tags": ["shell"], + "operationId": "v2.shell.timeout", "parameters": [ { - "name": "sessionID", + "name": "id", "in": "path", "schema": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - { - "$ref": "#/components/schemas/SessionNotFoundError" + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } - ] - } - } - } - } - }, - "description": "Switch the model used by subsequent provider turns.", - "summary": "Switch session model", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "model": { - "$ref": "#/components/schemas/ModelRef" - } + }, + "additionalProperties": false }, - "required": ["model"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.switchModel({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/prompt": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.prompt", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" + { + "type": "null" + } + ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -10523,11 +10291,14 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { - "$ref": "#/components/schemas/SessionInputAdmitted" + "$ref": "#/components/schemas/Shell.Info1" } }, - "required": ["data"], + "required": ["location", "data"], "additionalProperties": false } } @@ -10554,90 +10325,174 @@ } }, "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "409": { - "description": "ConflictError", + "description": "ShellNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConflictError" + "$ref": "#/components/schemas/ShellNotFoundError" } } } } }, - "description": "Durably admit one session input and schedule agent-loop execution unless resume is false.", - "summary": "Send message", + "description": "Replace a running shell command's timeout from now, or clear it with zero.", + "summary": "Update shell timeout", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/PromptInput" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - }, - "resume": { - "type": "boolean" + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, - "required": ["prompt"], + "required": ["timeout"], "additionalProperties": false } } }, "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.prompt({\n ...\n})" - } - ] + } } }, - "/api/session/{sessionID}/compact": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.compact", + "/api/shell/{id}/output": { + "get": { + "tags": ["shell"], + "operationId": "v2.shell.output", "parameters": [ { - "name": "sessionID", + "name": "id", "in": "path", "schema": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" + } + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" + } + ] + }, + "required": false } ], "security": [], "responses": { - "204": { - "description": "" + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": ["output", "cursor", "size", "truncated"], + "additionalProperties": false + } + }, + "required": ["location", "data"], + "additionalProperties": false + } + } + } }, "400": { "description": "InvalidRequestError", @@ -10660,132 +10515,131 @@ } }, "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "503": { - "description": "ServiceUnavailableError", + "description": "ShellNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" + "$ref": "#/components/schemas/ShellNotFoundError" } } } } }, - "description": "Compact a session conversation.", - "summary": "Compact session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.compact({\n ...\n})" - } - ] + "description": "Page through captured combined output by absolute byte cursor.", + "summary": "Read shell output" } }, - "/api/session/{sessionID}/wait": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.wait", + "/api/question/request": { + "get": { + "tags": ["question"], + "operationId": "v2.question.request.list", "parameters": [ { - "name": "sessionID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "pattern": "^ses" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Question.Request" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, - "404": { - "description": "SessionNotFoundError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "503": { - "description": "ServiceUnavailableError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Wait for a session agent loop to become idle.", - "summary": "Wait for session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.wait({\n ...\n})" - } - ] + "description": "Retrieve pending question requests for a location.", + "summary": "List pending question requests" } }, - "/api/session/{sessionID}/revert/stage": { - "post": { - "tags": ["sessions"], - "operationId": "v2.session.revert.stage", + "/api/session/{sessionID}/question": { + "get": { + "tags": ["question"], + "operationId": "v2.session.question.list", "parameters": [ { "name": "sessionID", "in": "path", "schema": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "required": true } @@ -10800,7 +10654,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/RevertState" + "type": "array", + "items": { + "$ref": "#/components/schemas/Question.Request" + } } }, "required": ["data"], @@ -10830,14 +10687,11 @@ } }, "404": { - "description": "MessageNotFoundError | SessionNotFoundError", + "description": "SessionNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ - { - "$ref": "#/components/schemas/MessageNotFoundError" - }, { "$ref": "#/components/schemas/SessionNotFoundError" }, @@ -10848,60 +10702,40 @@ } } } - }, - "500": { - "description": "UnknownError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnknownError1" - } - } - } } }, - "description": "Stage or move a reversible session boundary and optionally apply its file changes.", - "summary": "Stage session revert", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "files": { - "type": "boolean" - } - }, - "required": ["messageID"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.revert.stage({\n ...\n})" - } - ] + "description": "Retrieve pending question requests owned by a session.", + "summary": "List session question requests" } }, - "/api/session/{sessionID}/revert/clear": { + "/api/session/{sessionID}/question/{requestID}/reply": { "post": { - "tags": ["sessions"], - "operationId": "v2.session.revert.clear", + "tags": ["question"], + "operationId": "v2.session.question.reply", "parameters": [ { "name": "sessionID", "in": "path", "schema": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] }, "required": true } @@ -10932,11 +10766,14 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | QuestionNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, { "$ref": "#/components/schemas/SessionNotFoundError" }, @@ -10947,38 +10784,50 @@ } } } - }, - "500": { - "description": "UnknownError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnknownError1" - } - } - } } }, - "summary": "Clear staged revert", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.revert.clear({\n ...\n})" - } - ] + "description": "Answer a pending question request owned by a session.", + "summary": "Reply to pending question request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Question.Reply" + } + } + }, + "required": true + } } }, - "/api/session/{sessionID}/revert/commit": { + "/api/session/{sessionID}/question/{requestID}/reject": { "post": { - "tags": ["sessions"], - "operationId": "v2.session.revert.commit", + "tags": ["question"], + "operationId": "v2.session.question.reject", "parameters": [ { "name": "sessionID", "in": "path", "schema": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] }, "required": true } @@ -11009,11 +10858,14 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | QuestionNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, { "$ref": "#/components/schemas/SessionNotFoundError" }, @@ -11026,159 +10878,185 @@ } } }, - "summary": "Commit staged revert", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.revert.commit({\n ...\n})" - } - ] + "description": "Reject a pending question request owned by a session.", + "summary": "Reject pending question request" } }, - "/api/session/{sessionID}/context": { + "/api/reference": { "get": { - "tags": ["sessions"], - "operationId": "v2.session.context", + "tags": ["reference"], + "operationId": "v2.reference.list", "parameters": [ { - "name": "sessionID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { + "anyOf": [ + { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionMessage" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" } - } - } - }, - "401": { - "description": "UnauthorizedError", + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthorizedError" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference.Info" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, - "404": { - "description": "SessionNotFoundError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "500": { - "description": "UnknownError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnknownError1" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Retrieve the active context messages for a session (all messages after the last compaction).", - "summary": "Get session context", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.context({\n ...\n})" - } - ] + "description": "List references available in the requested location.", + "summary": "List references" } }, - "/api/session/{sessionID}/history": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.history", + "/api/experimental/project/{projectID}/copy": { + "post": { + "tags": ["projectCopy"], + "operationId": "v2.projectCopy.create", "parameters": [ { - "name": "sessionID", + "name": "projectID", "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "limit", - "in": "query", "schema": { "type": "string" }, - "required": false + "required": true }, { - "name": "after", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { "200": { - "description": "SessionHistory", + "description": "ProjectCopy.Copy", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionHistory" + "$ref": "#/components/schemas/ProjectCopy.Copy" } } } }, "400": { - "description": "InvalidRequestError", + "description": "ProjectCopyError | InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -11192,150 +11070,103 @@ } } } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } } }, - "description": "Read one finite page of public durable Session events after an exclusive aggregate sequence. Newly committed events may appear on later pages.", - "summary": "Get session history", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.history({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/event": { - "get": { - "tags": ["sessions"], - "operationId": "v2.session.events", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "strategy": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["strategy", "directory"], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": ["projectCopy"], + "operationId": "v2.projectCopy.remove", "parameters": [ { - "name": "sessionID", + "name": "projectID", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" + "type": "string" }, "required": true }, { - "name": "after", + "name": "location", "in": "query", "schema": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { - "200": { - "description": "Success", + "204": { + "description": "" + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", "content": { - "text/event-stream": { + "application/json": { "schema": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "event": { - "type": "string" + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" }, - "data": { - "$ref": "#/components/schemas/SessionDurableEventStream" + { + "$ref": "#/components/schemas/InvalidRequestError" } - }, - "required": ["id", "event", "data"], - "additionalProperties": false - }, - "x-effect-stream": { - "encoding": "sse", - "causeSchema": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Fail"] - }, - "error": { - "not": {} - } - }, - "required": ["_tag", "error"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Die"] - }, - "defect": {} - }, - "required": ["_tag", "defect"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Interrupt"] - }, - "fiberId": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - } - }, - "required": ["_tag", "fiberId"], - "additionalProperties": false - } - ] - } - }, - "errorSchema": { - "not": {} - }, - "failureEvent": "effect/httpapi/stream/failure" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + ] } } } @@ -11349,48 +11180,82 @@ } } } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } } }, - "description": "Replay durable events after an aggregate sequence, then continue with new durable events.", - "summary": "Subscribe to session events", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.events({\n ...\n})" - } - ] + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "force": { + "type": "boolean" + } + }, + "required": ["directory", "force"], + "additionalProperties": false + } + } + }, + "required": true + } } }, - "/api/session/{sessionID}/interrupt": { + "/api/experimental/project/{projectID}/copy/refresh": { "post": { - "tags": ["sessions"], - "operationId": "v2.session.interrupt", + "tags": ["projectCopy"], + "operationId": "v2.projectCopy.refresh", "parameters": [ { - "name": "sessionID", + "name": "projectID", "in": "path", "schema": { - "type": "string", - "pattern": "^ses" + "type": "string" }, "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -11399,11 +11264,18 @@ "description": "" }, "400": { - "description": "InvalidRequestError", + "description": "ProjectCopyError | InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -11417,57 +11289,54 @@ } } } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op.", - "summary": "Interrupt session execution", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.interrupt({\n ...\n})" } - ] + } } }, - "/api/session/{sessionID}/message/{messageID}": { + "/api/vcs": { "get": { - "tags": ["sessions"], - "operationId": "v2.session.message", + "tags": ["vcs"], + "operationId": "v2.vcs.get", "parameters": [ { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "messageID", - "in": "path", + "name": "location", + "in": "query", "schema": { - "type": "string", - "pattern": "^msg_" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": true + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], @@ -11479,11 +11348,14 @@ "schema": { "type": "object", "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, "data": { - "$ref": "#/components/schemas/SessionMessage" + "$ref": "#/components/schemas/Vcs.Info" } }, - "required": ["data"], + "required": ["location", "data"], "additionalProperties": false } } @@ -11508,104 +11380,89 @@ } } } - }, - "404": { - "description": "SessionNotFoundError | MessageNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/MessageNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } } }, - "description": "Retrieve one projected message owned by the Session.", - "summary": "Get session message", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.message({\n ...\n})" - } - ] + "description": "Get current and default branch information for the requested location.", + "summary": "VCS info" } }, - "/api/session/{sessionID}/message": { + "/api/vcs/status": { "get": { - "tags": ["messages"], - "operationId": "v2.session.messages", + "tags": ["vcs"], + "operationId": "v2.vcs.status", "parameters": [ { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "number" - }, - "required": false - }, - { - "name": "order", - "in": "query", - "schema": { - "type": "string", - "enum": ["asc", "desc"] - }, - "required": false - }, - { - "name": "cursor", + "name": "location", "in": "query", "schema": { - "type": "string", - "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order." + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] }, - "required": false + "required": false, + "style": "deepObject", + "explode": true } ], "security": [], "responses": { "200": { - "description": "SessionMessagesResponse", + "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionMessagesResponse" + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Vcs.FileStatus" + } + } + }, + "required": ["location", "data"], + "additionalProperties": false } } } }, "400": { - "description": "InvalidCursorError | InvalidRequestError", + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidCursorError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -11619,68 +11476,79 @@ } } } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - }, - "500": { - "description": "UnknownError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnknownError1" - } - } - } } }, - "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", - "summary": "Get session messages", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.messages({\n ...\n})" - } - ] + "description": "List uncommitted working-copy changes relative to the requested location.", + "summary": "VCS status" } }, - "/api/model": { + "/api/vcs/diff": { "get": { - "tags": ["models"], - "operationId": "v2.model.list", + "tags": ["vcs"], + "operationId": "v2.vcs.diff", "parameters": [ { "name": "location", "in": "query", "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, - "workspace": { - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, "required": false, "style": "deepObject", "explode": true + }, + { + "name": "mode", + "in": "query", + "schema": { + "$ref": "#/components/schemas/Vcs.Mode" + }, + "required": true + }, + { + "name": "context", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false } ], "security": [], @@ -11693,12 +11561,12 @@ "type": "object", "properties": { "location": { - "$ref": "#/components/schemas/LocationInfo" + "$ref": "#/components/schemas/Location.Info" }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ModelV2Info" + "$ref": "#/components/schemas/FileDiff.Info" } } }, @@ -11727,53 +11595,17 @@ } } } - }, - "503": { - "description": "ServiceUnavailableError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" - } - } - } } }, - "description": "Retrieve available models ordered by release date.", - "summary": "List models", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.model.list({\n ...\n})" - } - ] + "description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", + "summary": "VCS diff" } }, - "/api/provider": { + "/api/debug/location": { "get": { - "tags": ["providers"], - "operationId": "v2.provider.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], + "tags": ["debug"], + "operationId": "v2.debug.location.list", + "parameters": [], "security": [], "responses": { "200": { @@ -11781,20 +11613,10 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProviderV2Info" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false + "type": "array", + "items": { + "$ref": "#/components/schemas/Location.Ref" + } } } } @@ -11818,82 +11640,60 @@ } } } - }, - "503": { - "description": "ServiceUnavailableError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" - } - } - } } }, - "description": "Retrieve active AI providers so clients can show provider availability and configuration.", - "summary": "List providers", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.provider.list({\n ...\n})" - } - ] - } - }, - "/api/provider/{providerID}": { - "get": { - "tags": ["providers"], - "operationId": "v2.provider.get", + "description": "List locations currently loaded by the server.", + "summary": "List loaded locations" + }, + "delete": { + "tags": ["debug"], + "operationId": "v2.debug.location.evict", "parameters": [ - { - "name": "providerID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, { "name": "location", "in": "query", "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { + "anyOf": [ + { "type": "object", "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "data": { - "$ref": "#/components/schemas/ProviderV2Info" + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, - "required": ["location", "data"], "additionalProperties": false + }, + { + "type": "null" } - } - } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" }, "400": { "description": "InvalidRequestError", @@ -11914,57 +11714,171 @@ } } } + } + }, + "description": "Dispose the requested location's cached services so its next use boots them fresh.", + "summary": "Evict a loaded location" + } + }, + "/api/experimental/migration/v1": { + "get": { + "tags": ["migration"], + "operationId": "v2.experimental.migration.v1.status", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["required", "completed"] + } + }, + "required": ["status"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["running"] + }, + "progress": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "numerator": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "denominator": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": ["label"], + "additionalProperties": false + } + }, + "required": ["status", "progress"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["error"] + }, + "error": { + "type": "string" + } + }, + "required": ["status", "error"], + "additionalProperties": false + } + ] + } + } + } }, - "404": { - "description": "ProviderNotFoundError", + "400": { + "description": "InvalidRequestError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProviderNotFoundError" + "$ref": "#/components/schemas/InvalidRequestError" } } } }, - "503": { - "description": "ServiceUnavailableError", + "401": { + "description": "UnauthorizedError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" + "$ref": "#/components/schemas/UnauthorizedError" } } } } }, - "description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", - "summary": "Get provider", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.provider.get({\n ...\n})" - } - ] + "description": "Return the progress of the V1 to V2 session history migration.", + "summary": "Get V1 migration status" } }, - "/api/integration": { + "/api/websearch/provider": { "get": { - "tags": ["integrations"], - "operationId": "v2.integration.list", + "tags": ["websearch"], + "operationId": "v2.websearch.providers", "parameters": [ { "name": "location", "in": "query", "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, - "workspace": { - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, "required": false, "style": "deepObject", @@ -11981,12 +11895,12 @@ "type": "object", "properties": { "location": { - "$ref": "#/components/schemas/LocationInfo" + "$ref": "#/components/schemas/Location.Info" }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/IntegrationInfo" + "$ref": "#/components/schemas/WebSearch.Provider" } } }, @@ -12015,45 +11929,62 @@ } } } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } } }, - "description": "Retrieve available integrations and their authentication methods.", - "summary": "List integrations", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.list({\n ...\n})" - } - ] + "description": "Return the registered web search providers.", + "summary": "List web search providers" } }, - "/api/integration/{integrationID}": { - "get": { - "tags": ["integrations"], - "operationId": "v2.integration.get", + "/api/websearch": { + "post": { + "tags": ["websearch"], + "operationId": "v2.websearch.query", "parameters": [ - { - "name": "integrationID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, { "name": "location", "in": "query", "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, - "workspace": { - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, "required": false, "style": "deepObject", @@ -12070,10 +12001,10 @@ "type": "object", "properties": { "location": { - "$ref": "#/components/schemas/LocationInfo" + "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/IntegrationInfo" + "$ref": "#/components/schemas/WebSearch.Response" } }, "required": ["location", "data"], @@ -12087,7 +12018,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -12101,260 +12039,82 @@ } } } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } } }, - "description": "Retrieve one integration and its authentication methods.", - "summary": "Get integration", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.get({\n ...\n})" - } - ] - } - }, - "/api/integration/{integrationID}/connect/key": { - "post": { - "tags": ["integrations"], - "operationId": "v2.integration.connect.key", - "parameters": [ - { - "name": "integrationID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Run a key authentication method and store the resulting credential.", - "summary": "Connect with key", + "description": "Run one web search through the selected provider. Specify a provider to override the configured default.", + "summary": "Search the web", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "key": { + "query": { "type": "string" }, - "label": { + "providerID": { "type": "string" } }, - "required": ["key"], + "required": ["query"], "additionalProperties": false } } }, "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.connect.key({\n ...\n})" - } - ] + } } }, - "/api/integration/{integrationID}/connect/oauth": { - "post": { - "tags": ["integrations"], - "operationId": "v2.integration.connect.oauth", + "/api/config": { + "get": { + "tags": ["config"], + "operationId": "v2.config.get", "parameters": [ - { - "name": "integrationID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, { "name": "location", "in": "query", "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { + "anyOf": [ + { "type": "object", "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "data": { - "$ref": "#/components/schemas/IntegrationAttempt" + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, - "required": ["location", "data"], "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Start an OAuth attempt and return the authorization details.", - "summary": "Begin OAuth connection", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "methodID": { - "type": "string" - }, - "inputs": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "label": { - "type": "string" - } - }, - "required": ["methodID", "inputs"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.connect.oauth({\n ...\n})" - } - ] - } - }, - "/api/integration/attempt/{attemptID}": { - "get": { - "tags": ["integrations"], - "operationId": "v2.integration.attempt.status", - "parameters": [ - { - "name": "attemptID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" }, - "workspace": { - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, "required": false, "style": "deepObject", @@ -12368,17 +12128,10 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/IntegrationAttemptStatus" - } - }, - "required": ["location", "data"], - "additionalProperties": false + "type": "array", + "items": { + "$ref": "#/components/schemas/Config.Entry" + } } } } @@ -12404,15589 +12157,3222 @@ } } }, - "description": "Poll the current status of an OAuth attempt.", - "summary": "Get OAuth attempt status", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.attempt.status({\n ...\n})" + "description": "Return configuration documents and discovery sources for the requested location, from lowest to highest priority.", + "summary": "Get configuration" + } + } + }, + "components": { + "schemas": { + "ServiceHealth": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean", + "enum": [true] + }, + "version": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } - ] + }, + "required": ["healthy", "version", "pid"], + "additionalProperties": false }, - "delete": { - "tags": ["integrations"], - "operationId": "v2.integration.attempt.cancel", - "parameters": [ - { - "name": "attemptID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true + "UnauthorizedError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["UnauthorizedError"] }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true + "message": { + "type": "string" } - ], - "security": [], - "responses": { - "204": { - "description": "" + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "InvalidRequestError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["InvalidRequestError"] }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } + "message": { + "type": "string" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ] }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } + "field": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ] } }, - "description": "Cancel an OAuth attempt and release its resources.", - "summary": "Cancel OAuth connection", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.attempt.cancel({\n ...\n})" + "required": ["_tag", "message"], + "additionalProperties": false + }, + "ServiceStopRequest": { + "type": "object", + "properties": { + "instanceID": { + "type": "string" } - ] - } - }, - "/api/integration/attempt/{attemptID}/complete": { - "post": { - "tags": ["integrations"], - "operationId": "v2.integration.attempt.complete", - "parameters": [ - { - "name": "attemptID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true + }, + "required": ["instanceID"], + "additionalProperties": false + }, + "ServiceStopResponse": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean" } - ], - "security": [], - "responses": { - "204": { - "description": "" + }, + "required": ["accepted"], + "additionalProperties": false + }, + "Location.Info": { + "type": "object", + "properties": { + "directory": { + "type": "string" }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" } - } + ] }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } + "project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "canonical": { + "type": "string" } - } + }, + "required": ["id", "directory", "canonical"], + "additionalProperties": false } }, - "description": "Complete a code-based OAuth attempt and store the resulting credential.", - "summary": "Complete OAuth connection", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": { - "type": "string" - } - }, - "additionalProperties": false - } - } + "required": ["directory", "project"], + "additionalProperties": false + }, + "Model.Ref": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.integration.attempt.complete({\n ...\n})" - } - ] - } - }, - "/api/credential/{credentialID}": { - "patch": { - "tags": ["opencode HttpApi"], - "operationId": "v2.credential.update", - "parameters": [ - { - "name": "credentialID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true + "providerID": { + "type": "string" }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true + "variant": { + "type": "string" } - ], - "security": [], - "responses": { - "204": { - "description": "" + }, + "required": ["id", "providerID"], + "additionalProperties": false + }, + "Provider.Settings": { + "type": "object" + }, + "Provider.Request": { + "type": "object", + "properties": { + "settings": { + "$ref": "#/components/schemas/Provider.Settings" }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" } }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } + "body": { + "type": "object" } }, - "description": "Update a stored credential label.", - "summary": "Update credential", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "label": { - "type": "string" - } - }, - "required": ["label"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.credential.update({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["opencode HttpApi"], - "operationId": "v2.credential.remove", - "parameters": [ - { - "name": "credentialID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Remove a stored integration credential.", - "summary": "Remove credential", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.credential.remove({\n ...\n})" - } - ] - } - }, - "/api/permission/request": { - "get": { - "tags": ["permissions"], - "operationId": "v2.permission.request.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2Request" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve pending permission requests for a location.", - "summary": "List pending permission requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.permission.request.list({\n ...\n})" - } - ] - } - }, - "/api/permission/saved": { - "get": { - "tags": ["permissions"], - "operationId": "v2.permission.saved.list", - "parameters": [ - { - "name": "projectID", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionSavedInfo" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve saved permissions, optionally filtered by project.", - "summary": "List saved permissions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.permission.saved.list({\n ...\n})" - } - ] - } - }, - "/api/permission/saved/{id}": { - "delete": { - "tags": ["permissions"], - "operationId": "v2.permission.saved.remove", - "parameters": [ - { - "name": "id", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Remove a saved permission by ID.", - "summary": "Remove saved permission", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.permission.saved.remove({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/permission": { - "post": { - "tags": ["permissions"], - "operationId": "v2.session.permission.create", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "effect": { - "$ref": "#/components/schemas/PermissionV2Effect" - } - }, - "required": ["id", "effect"], - "additionalProperties": false - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Evaluate and, when approval is required, create a permission request for a session.", - "summary": "Create permission request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - }, - "agent": { - "type": "string" - } - }, - "required": ["action", "resources"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.create({\n ...\n})" - } - ] - }, - "get": { - "tags": ["permissions"], - "operationId": "v2.session.permission.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2Request" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Retrieve pending permission requests owned by a session.", - "summary": "List session permission requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.list({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/permission/{requestID}": { - "get": { - "tags": ["permissions"], - "operationId": "v2.session.permission.get", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^per" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/PermissionV2Request" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Retrieve a pending permission request owned by a session.", - "summary": "Get permission request", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.get({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/permission/{requestID}/reply": { - "post": { - "tags": ["permissions"], - "operationId": "v2.session.permission.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^per" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Respond to a pending permission request owned by a session.", - "summary": "Reply to pending permission request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" - }, - "message": { - "type": "string" - } - }, - "required": ["reply"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.reply({\n ...\n})" - } - ] - } - }, - "/api/fs/read/*": { - "get": { - "tags": ["filesystem"], - "operationId": "v2.fs.read", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Serve one file relative to the requested location.", - "summary": "Read file", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.fs.read({\n ...\n})" - } - ] - } - }, - "/api/fs/list": { - "get": { - "tags": ["filesystem"], - "operationId": "v2.fs.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - }, - { - "name": "path", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileSystemEntry" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "List direct children of one directory relative to the requested location.", - "summary": "List directory", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.fs.list({\n ...\n})" - } - ] - } - }, - "/api/fs/find": { - "get": { - "tags": ["filesystem"], - "operationId": "v2.fs.find", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - }, - { - "name": "query", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "type", - "in": "query", - "schema": { - "type": "string", - "enum": ["file", "directory"] - }, - "required": false - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileSystemEntry" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Find recursively ranked filesystem entries relative to the requested location.", - "summary": "Find files", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.fs.find({\n ...\n})" - } - ] - } - }, - "/api/command": { - "get": { - "tags": ["commands"], - "operationId": "v2.command.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CommandV2Info" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve currently registered commands.", - "summary": "List commands", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.command.list({\n ...\n})" - } - ] - } - }, - "/api/skill": { - "get": { - "tags": ["skills"], - "operationId": "v2.skill.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SkillV2Info" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve currently registered skills.", - "summary": "List skills", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.skill.list({\n ...\n})" - } - ] - } - }, - "/api/event": { - "get": { - "tags": ["events"], - "operationId": "v2.event.subscribe", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Event stream", - "content": { - "text/event-stream": { - "schema": { - "$ref": "#/components/schemas/V2Event" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Subscribe to native event payloads for the server.", - "summary": "Subscribe to events", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.event.subscribe({\n ...\n})" - } - ] - } - }, - "/api/pty": { - "get": { - "tags": ["pty"], - "operationId": "v2.pty.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pty" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "List PTY sessions for a location, including exited sessions retained until removal.", - "summary": "List PTY sessions", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.list({\n ...\n})" - } - ] - }, - "post": { - "tags": ["pty"], - "operationId": "v2.pty.create", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Create a pseudo-terminal session for a location.", - "summary": "Create PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "title": { - "type": "string" - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.create({\n ...\n})" - } - ] - } - }, - "/api/pty/{ptyID}": { - "get": { - "tags": ["pty"], - "operationId": "v2.pty.get", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Get one PTY session, including its exit code once exited.", - "summary": "Get PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.get({\n ...\n})" - } - ] - }, - "put": { - "tags": ["pty"], - "operationId": "v2.pty.update", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Update the title or viewport size of one PTY session.", - "summary": "Update PTY session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "size": { - "type": "object", - "properties": { - "rows": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "cols": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["rows", "cols"], - "additionalProperties": false - } - }, - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.update({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["pty"], - "operationId": "v2.pty.remove", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Terminate and remove one PTY session.", - "summary": "Remove PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.remove({\n ...\n})" - } - ] - } - }, - "/api/pty/{ptyID}/connect-token": { - "post": { - "tags": ["pty"], - "operationId": "v2.pty.connectToken", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "$ref": "#/components/schemas/PtyTicketConnectToken" - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "403": { - "description": "ForbiddenError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForbiddenError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", - "summary": "Create PTY WebSocket token", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.connectToken({\n ...\n})" - } - ] - } - }, - "/api/pty/{ptyID}/connect": { - "get": { - "tags": ["pty"], - "operationId": "v2.pty.connect", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "in": "query", - "name": "location[directory]", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "location[workspace]", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "cursor", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "ticket", - "schema": { - "type": "string" - } - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "403": { - "description": "ForbiddenError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForbiddenError" - } - } - } - }, - "404": { - "description": "PtyNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PtyNotFoundError" - } - } - } - } - }, - "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", - "summary": "Connect to PTY session", - "x-websocket": true, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.pty.connect({\n ...\n})" - } - ] - } - }, - "/api/question/request": { - "get": { - "tags": ["session questions"], - "operationId": "v2.question.request.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Request" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve pending question requests for a location.", - "summary": "List pending question requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.question.request.list({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/question": { - "get": { - "tags": ["session questions"], - "operationId": "v2.session.question.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Request" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Retrieve pending question requests owned by a session.", - "summary": "List session question requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.question.list({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/question/{requestID}/reply": { - "post": { - "tags": ["session questions"], - "operationId": "v2.session.question.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^que" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | QuestionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Answer a pending question request owned by a session.", - "summary": "Reply to pending question request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuestionV2Reply" - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.question.reply({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/question/{requestID}/reject": { - "post": { - "tags": ["session questions"], - "operationId": "v2.session.question.reject", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^que" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | QuestionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Reject a pending question request owned by a session.", - "summary": "Reject pending question request", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.question.reject({\n ...\n})" - } - ] - } - }, - "/api/reference": { - "get": { - "tags": ["reference"], - "operationId": "v2.reference.list", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/LocationInfo" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ReferenceInfo" - } - } - }, - "required": ["location", "data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "List references available in the requested location.", - "summary": "List references", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.reference.list({\n ...\n})" - } - ] - } - }, - "/experimental/project/{projectID}/copy": { - "post": { - "tags": ["projectCopy"], - "operationId": "v2.projectCopy.create", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "responses": { - "200": { - "description": "ProjectCopy.Copy", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProjectCopyCopy" - } - } - } - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "strategy": { - "type": "string" - }, - "directory": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["strategy", "directory"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.projectCopy.create({\n ...\n})" - } - ] - }, - "delete": { - "tags": ["projectCopy"], - "operationId": "v2.projectCopy.remove", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "force": { - "type": "boolean" - } - }, - "required": ["directory", "force"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.projectCopy.remove({\n ...\n})" - } - ] - } - }, - "/experimental/project/{projectID}/copy/refresh": { - "post": { - "tags": ["projectCopy"], - "operationId": "v2.projectCopy.refresh", - "parameters": [ - { - "name": "projectID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspace": { - "type": "string" - } - }, - "additionalProperties": false - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "ProjectCopyError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectCopyError" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.projectCopy.refresh({\n ...\n})" - } - ] - } - }, - "/pty/{ptyID}/connect": { - "get": { - "tags": ["pty"], - "operationId": "pty.connect", - "parameters": [ - { - "name": "ptyID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^pty" - }, - "required": true - }, - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "cursor", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "ticket", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Connected session", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Connected session" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/effect_HttpApiError_Forbidden" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "description": "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.", - "summary": "Connect to PTY session", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.connect({\n ...\n})" - } - ] - } - } - }, - "components": { - "schemas": { - "Event": { - "anyOf": [ - { - "$ref": "#/components/schemas/EventModels-devRefreshed" - }, - { - "$ref": "#/components/schemas/EventIntegrationUpdated" - }, - { - "$ref": "#/components/schemas/EventIntegrationConnectionUpdated" - }, - { - "$ref": "#/components/schemas/EventCatalogUpdated" - }, - { - "$ref": "#/components/schemas/EventSessionCreated" - }, - { - "$ref": "#/components/schemas/EventSessionUpdated" - }, - { - "$ref": "#/components/schemas/EventSessionDeleted" - }, - { - "$ref": "#/components/schemas/EventMessageUpdated" - }, - { - "$ref": "#/components/schemas/EventMessageRemoved" - }, - { - "$ref": "#/components/schemas/EventMessagePartUpdated" - }, - { - "$ref": "#/components/schemas/EventMessagePartRemoved" - }, - { - "$ref": "#/components/schemas/EventSessionNextAgentSwitched" - }, - { - "$ref": "#/components/schemas/EventSessionNextModelSwitched" - }, - { - "$ref": "#/components/schemas/EventSessionNextMoved" - }, - { - "$ref": "#/components/schemas/EventSessionNextPrompted" - }, - { - "$ref": "#/components/schemas/EventSessionNextPromptAdmitted" - }, - { - "$ref": "#/components/schemas/EventSessionNextContextUpdated" - }, - { - "$ref": "#/components/schemas/EventSessionNextSynthetic" - }, - { - "$ref": "#/components/schemas/EventSessionNextShellStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextShellEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextStepStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextStepEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextStepFailed" - }, - { - "$ref": "#/components/schemas/EventSessionNextTextStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextTextDelta" - }, - { - "$ref": "#/components/schemas/EventSessionNextTextEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextReasoningStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextReasoningDelta" - }, - { - "$ref": "#/components/schemas/EventSessionNextReasoningEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolInputStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolInputDelta" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolInputEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolCalled" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolProgress" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolSuccess" - }, - { - "$ref": "#/components/schemas/EventSessionNextToolFailed" - }, - { - "$ref": "#/components/schemas/EventSessionNextRetried" - }, - { - "$ref": "#/components/schemas/EventSessionNextCompactionStarted" - }, - { - "$ref": "#/components/schemas/EventSessionNextCompactionDelta" - }, - { - "$ref": "#/components/schemas/EventSessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/EventSessionNextRevertStaged" - }, - { - "$ref": "#/components/schemas/EventSessionNextRevertCleared" - }, - { - "$ref": "#/components/schemas/EventSessionNextRevertCommitted" - }, - { - "$ref": "#/components/schemas/EventMessagePartDelta" - }, - { - "$ref": "#/components/schemas/EventSessionDiff" - }, - { - "$ref": "#/components/schemas/EventSessionError" - }, - { - "$ref": "#/components/schemas/EventInstallationUpdated" - }, - { - "$ref": "#/components/schemas/EventInstallationUpdate-available" - }, - { - "$ref": "#/components/schemas/EventFileEdited" - }, - { - "$ref": "#/components/schemas/EventReferenceUpdated" - }, - { - "$ref": "#/components/schemas/EventPermissionV2Asked" - }, - { - "$ref": "#/components/schemas/EventPermissionV2Replied" - }, - { - "$ref": "#/components/schemas/EventPluginAdded" - }, - { - "$ref": "#/components/schemas/EventProjectDirectoriesUpdated" - }, - { - "$ref": "#/components/schemas/EventFileWatcherUpdated" - }, - { - "$ref": "#/components/schemas/EventPtyCreated" - }, - { - "$ref": "#/components/schemas/EventPtyUpdated" - }, - { - "$ref": "#/components/schemas/EventPtyExited" - }, - { - "$ref": "#/components/schemas/EventPtyDeleted" - }, - { - "$ref": "#/components/schemas/EventQuestionV2Asked" - }, - { - "$ref": "#/components/schemas/EventQuestionV2Replied" - }, - { - "$ref": "#/components/schemas/EventQuestionV2Rejected" - }, - { - "$ref": "#/components/schemas/EventTodoUpdated" - }, - { - "$ref": "#/components/schemas/EventLspUpdated" - }, - { - "$ref": "#/components/schemas/EventPermissionAsked" - }, - { - "$ref": "#/components/schemas/EventPermissionReplied" - }, - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/Event.tui.toast.show" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, - { - "$ref": "#/components/schemas/EventMcpToolsChanged" - }, - { - "$ref": "#/components/schemas/EventMcpBrowserOpenFailed" - }, - { - "$ref": "#/components/schemas/EventCommandExecuted" - }, - { - "$ref": "#/components/schemas/EventProjectUpdated" - }, - { - "$ref": "#/components/schemas/EventSessionStatus" - }, - { - "$ref": "#/components/schemas/EventSessionIdle" - }, - { - "$ref": "#/components/schemas/EventQuestionAsked" - }, - { - "$ref": "#/components/schemas/EventQuestionReplied" - }, - { - "$ref": "#/components/schemas/EventQuestionRejected" - }, - { - "$ref": "#/components/schemas/EventSessionCompacted" - }, - { - "$ref": "#/components/schemas/EventVcsBranchUpdated" - }, - { - "$ref": "#/components/schemas/EventWorkspaceReady" - }, - { - "$ref": "#/components/schemas/EventWorkspaceFailed" - }, - { - "$ref": "#/components/schemas/EventWorkspaceStatus" - }, - { - "$ref": "#/components/schemas/EventWorktreeReady" - }, - { - "$ref": "#/components/schemas/EventWorktreeFailed" - }, - { - "$ref": "#/components/schemas/EventServerConnected" - }, - { - "$ref": "#/components/schemas/EventGlobalDisposed" - }, - { - "$ref": "#/components/schemas/EventServerInstanceDisposed" - } - ] - }, - "QuestionReplied": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - }, - "QuestionRejected": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - }, - "OAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["oauth"] - }, - "refresh": { - "type": "string" - }, - "access": { - "type": "string" - }, - "expires": { - "type": "integer", - "minimum": 0 - }, - "accountId": { - "type": "string" - }, - "enterpriseUrl": { - "type": "string" - } - }, - "required": ["type", "refresh", "access", "expires"], - "additionalProperties": false - }, - "ApiAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["api"] - }, - "key": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["type", "key"], - "additionalProperties": false - }, - "WellKnownAuth": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["wellknown"] - }, - "key": { - "type": "string" - }, - "token": { - "type": "string" - } - }, - "required": ["type", "key", "token"], - "additionalProperties": false - }, - "Auth": { - "anyOf": [ - { - "$ref": "#/components/schemas/OAuth" - }, - { - "$ref": "#/components/schemas/ApiAuth" - }, - { - "$ref": "#/components/schemas/WellKnownAuth" - } - ] - }, - "effect_HttpApiError_BadRequest": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["BadRequest"] - } - }, - "required": ["_tag"], - "additionalProperties": false - }, - "InvalidRequestError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["InvalidRequestError"] - }, - "message": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "field": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "MoveSessionError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["MoveSessionError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "SnapshotFileDiff": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "patch": { - "type": "string" - }, - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "status": { - "type": "string", - "enum": ["added", "deleted", "modified"] - } - }, - "required": ["additions", "deletions"], - "additionalProperties": false - }, - "PermissionAction": { - "type": "string", - "enum": ["allow", "deny", "ask"] - }, - "PermissionRule": { - "type": "object", - "properties": { - "permission": { - "type": "string" - }, - "pattern": { - "type": "string" - }, - "action": { - "$ref": "#/components/schemas/PermissionAction" - } - }, - "required": ["permission", "pattern", "action"], - "additionalProperties": false - }, - "PermissionRuleset": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionRule" - } - }, - "Session": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^ses" - }, - "slug": { - "type": "string" - }, - "projectID": { - "type": "string" - }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "directory": { - "type": "string" - }, - "path": { - "type": "string" - }, - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "summary": { - "type": "object", - "properties": { - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "files": { - "type": "number" - }, - "diffs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["additions", "deletions", "files"], - "additionalProperties": false - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "share": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "additionalProperties": false - }, - "title": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["id", "providerID"], - "additionalProperties": false - }, - "version": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - }, - "updated": { - "type": "integer", - "minimum": 0 - }, - "compacting": { - "type": "integer", - "minimum": 0 - }, - "archived": { - "type": "number" - } - }, - "required": ["created", "updated"], - "additionalProperties": false - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "revert": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "snapshot": { - "type": "string" - }, - "diff": { - "type": "string" - } - }, - "required": ["messageID"], - "additionalProperties": false - } - }, - "required": ["id", "slug", "projectID", "directory", "title", "version", "time"], - "additionalProperties": false - }, - "OutputFormatText": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - } - }, - "required": ["type"], - "additionalProperties": false - }, - "JSONSchema": { - "type": "object" - }, - "OutputFormatJsonSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["json_schema"] - }, - "schema": { - "$ref": "#/components/schemas/JSONSchema" - }, - "retryCount": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["type", "schema"], - "additionalProperties": false - }, - "OutputFormat": { - "anyOf": [ - { - "$ref": "#/components/schemas/OutputFormatText" - }, - { - "$ref": "#/components/schemas/OutputFormatJsonSchema" - } - ] - }, - "UserMessage": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "role": { - "type": "string", - "enum": ["user"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number", - "minimum": 0 - } - }, - "required": ["created"], - "additionalProperties": false - }, - "format": { - "$ref": "#/components/schemas/OutputFormat" - }, - "summary": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "body": { - "type": "string" - }, - "diffs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["diffs"], - "additionalProperties": false - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "system": { - "type": "string" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - } - }, - "required": ["id", "sessionID", "role", "time", "agent", "model"], - "additionalProperties": false - }, - "ProviderAuthError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["ProviderAuthError"] - }, - "data": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["providerID", "message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "UnknownError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["UnknownError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "ref": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "MessageOutputLengthError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["MessageOutputLengthError"] - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "MessageAbortedError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["MessageAbortedError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "StructuredOutputError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["StructuredOutputError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "retries": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["message", "retries"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "ContextOverflowError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["ContextOverflowError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "responseBody": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "ContentFilterError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["ContentFilterError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "APIError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["APIError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "statusCode": { - "type": "integer", - "minimum": 0 - }, - "isRetryable": { - "type": "boolean" - }, - "responseHeaders": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "responseBody": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["message", "isRetryable"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "AssistantMessage": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^msg" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "role": { - "type": "string", - "enum": ["assistant"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - }, - "completed": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["created"], - "additionalProperties": false - }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/StructuredOutputError" - }, - { - "$ref": "#/components/schemas/ContextOverflowError" - }, - { - "$ref": "#/components/schemas/ContentFilterError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - }, - "parentID": { - "type": "string", - "pattern": "^msg" - }, - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "path": { - "type": "object", - "properties": { - "cwd": { - "type": "string" - }, - "root": { - "type": "string" - } - }, - "required": ["cwd", "root"], - "additionalProperties": false - }, - "summary": { - "type": "boolean" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "total": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "structured": {}, - "variant": { - "type": "string" - }, - "finish": { - "type": "string" - } - }, - "required": [ - "id", - "sessionID", - "role", - "time", - "parentID", - "modelID", - "providerID", - "mode", - "agent", - "path", - "cost", - "tokens" - ], - "additionalProperties": false - }, - "Message": { - "anyOf": [ - { - "$ref": "#/components/schemas/UserMessage" - }, - { - "$ref": "#/components/schemas/AssistantMessage" - } - ] - }, - "TextPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["text"] - }, - "text": { - "type": "string" - }, - "synthetic": { - "type": "boolean" - }, - "ignored": { - "type": "boolean" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start"], - "additionalProperties": false - }, - "metadata": { - "type": "object" - } - }, - "required": ["id", "sessionID", "messageID", "type", "text"], - "additionalProperties": false - }, - "SubtaskPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["subtask"] - }, - "prompt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "command": { - "type": "string" - } - }, - "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"], - "additionalProperties": false - }, - "ReasoningPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["reasoning"] - }, - "text": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "messageID", "type", "text", "time"], - "additionalProperties": false - }, - "FilePartSourceText": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": ["value", "start", "end"], - "additionalProperties": false - }, - "FileSource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/FilePartSourceText" - }, - "type": { - "type": "string", - "enum": ["file"] - }, - "path": { - "type": "string" - } - }, - "required": ["text", "type", "path"], - "additionalProperties": false - }, - "Range": { - "type": "object", - "properties": { - "start": { - "type": "object", - "properties": { - "line": { - "type": "integer", - "minimum": 0 - }, - "character": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["line", "character"], - "additionalProperties": false - }, - "end": { - "type": "object", - "properties": { - "line": { - "type": "integer", - "minimum": 0 - }, - "character": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["line", "character"], - "additionalProperties": false - } - }, - "required": ["start", "end"], - "additionalProperties": false - }, - "SymbolSource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/FilePartSourceText" - }, - "type": { - "type": "string", - "enum": ["symbol"] - }, - "path": { - "type": "string" - }, - "range": { - "$ref": "#/components/schemas/Range" - }, - "name": { - "type": "string" - }, - "kind": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["text", "type", "path", "range", "name", "kind"], - "additionalProperties": false - }, - "ResourceSource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/FilePartSourceText" - }, - "type": { - "type": "string", - "enum": ["resource"] - }, - "clientName": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": ["text", "type", "clientName", "uri"], - "additionalProperties": false - }, - "FilePartSource": { - "anyOf": [ - { - "$ref": "#/components/schemas/FileSource" - }, - { - "$ref": "#/components/schemas/SymbolSource" - }, - { - "$ref": "#/components/schemas/ResourceSource" - } - ] - }, - "FilePart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["file"] - }, - "mime": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/FilePartSource" - } - }, - "required": ["id", "sessionID", "messageID", "type", "mime", "url"], - "additionalProperties": false - }, - "ToolStatePending": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["pending"] - }, - "input": { - "type": "object" - }, - "raw": { - "type": "string" - } - }, - "required": ["status", "input", "raw"], - "additionalProperties": false - }, - "ToolStateRunning": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["running"] - }, - "input": { - "type": "object" - }, - "title": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start"], - "additionalProperties": false - } - }, - "required": ["status", "input", "time"], - "additionalProperties": false - }, - "ToolStateCompleted": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["completed"] - }, - "input": { - "type": "object" - }, - "output": { - "type": "string" - }, - "title": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - }, - "compacted": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start", "end"], - "additionalProperties": false - }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FilePart" - } - } - }, - "required": ["status", "input", "output", "title", "metadata", "time"], - "additionalProperties": false - }, - "ToolStateError": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["error"] - }, - "input": { - "type": "object" - }, - "error": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start", "end"], - "additionalProperties": false - } - }, - "required": ["status", "input", "error", "time"], - "additionalProperties": false - }, - "ToolState": { - "anyOf": [ - { - "$ref": "#/components/schemas/ToolStatePending" - }, - { - "$ref": "#/components/schemas/ToolStateRunning" - }, - { - "$ref": "#/components/schemas/ToolStateCompleted" - }, - { - "$ref": "#/components/schemas/ToolStateError" - } - ] - }, - "ToolPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["tool"] - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "state": { - "$ref": "#/components/schemas/ToolState" - }, - "metadata": { - "type": "object" - } - }, - "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"], - "additionalProperties": false - }, - "StepStartPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["step-start"] - }, - "snapshot": { - "type": "string" - } - }, - "required": ["id", "sessionID", "messageID", "type"], - "additionalProperties": false - }, - "StepFinishPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["step-finish"] - }, - "reason": { - "type": "string" - }, - "snapshot": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "total": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"], - "additionalProperties": false - }, - "SnapshotPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["snapshot"] - }, - "snapshot": { - "type": "string" - } - }, - "required": ["id", "sessionID", "messageID", "type", "snapshot"], - "additionalProperties": false - }, - "PatchPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["patch"] - }, - "hash": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["id", "sessionID", "messageID", "type", "hash", "files"], - "additionalProperties": false - }, - "AgentPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["agent"] - }, - "name": { - "type": "string" - }, - "source": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["value", "start", "end"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "messageID", "type", "name"], - "additionalProperties": false - }, - "RetryPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["retry"] - }, - "attempt": { - "type": "integer", - "minimum": 0 - }, - "error": { - "$ref": "#/components/schemas/APIError" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["created"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"], - "additionalProperties": false - }, - "CompactionPart": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "type": { - "type": "string", - "enum": ["compaction"] - }, - "auto": { - "type": "boolean" - }, - "overflow": { - "type": "boolean" - }, - "tail_start_id": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["id", "sessionID", "messageID", "type", "auto"], - "additionalProperties": false - }, - "Part": { - "anyOf": [ - { - "$ref": "#/components/schemas/TextPart" - }, - { - "$ref": "#/components/schemas/SubtaskPart" - }, - { - "$ref": "#/components/schemas/ReasoningPart" - }, - { - "$ref": "#/components/schemas/FilePart" - }, - { - "$ref": "#/components/schemas/ToolPart" - }, - { - "$ref": "#/components/schemas/StepStartPart" - }, - { - "$ref": "#/components/schemas/StepFinishPart" - }, - { - "$ref": "#/components/schemas/SnapshotPart" - }, - { - "$ref": "#/components/schemas/PatchPart" - }, - { - "$ref": "#/components/schemas/AgentPart" - }, - { - "$ref": "#/components/schemas/RetryPart" - }, - { - "$ref": "#/components/schemas/CompactionPart" - } - ] - }, - "Prompt": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptFileAttachment" - } - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptAgentAttachment" - } - } - }, - "required": ["text"], - "additionalProperties": false - }, - "Pty": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - }, - "title": { - "type": "string" - }, - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["running", "exited"] - }, - "pid": { - "type": "integer", - "minimum": 0 - }, - "exitCode": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["id", "title", "command", "args", "cwd", "status", "pid"], - "additionalProperties": false - }, - "Todo": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "Brief description of the task" - }, - "status": { - "type": "string", - "description": "Current status of the task: pending, in_progress, completed, cancelled" - }, - "priority": { - "type": "string", - "description": "Priority level of the task: high, medium, low" - } - }, - "required": ["content", "status", "priority"], - "additionalProperties": false - }, - "SessionStatus": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["idle"] - } - }, - "required": ["type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["retry"] - }, - "attempt": { - "type": "integer", - "minimum": 0 - }, - "message": { - "type": "string" - }, - "action": { - "type": "object", - "properties": { - "reason": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "label": { - "type": "string" - }, - "link": { - "type": "string" - } - }, - "required": ["reason", "provider", "title", "message", "label"], - "additionalProperties": false - }, - "next": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["type", "attempt", "message", "next"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["busy"] - } - }, - "required": ["type"], - "additionalProperties": false - } - ] - }, - "QuestionOption": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" - }, - "description": { - "type": "string", - "description": "Explanation of choice" - } - }, - "required": ["label", "description"], - "additionalProperties": false - }, - "QuestionInfo": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { - "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionOption" - }, - "description": "Available choices" - }, - "multiple": { - "type": "boolean" - }, - "custom": { - "type": "boolean" - } - }, - "required": ["question", "header", "options"], - "additionalProperties": false - }, - "QuestionTool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - }, - "QuestionAnswer": { - "type": "array", - "items": { - "type": "string" - } - }, - "GlobalEvent": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "project": { - "type": "string" - }, - "workspace": { - "type": "string" - }, - "payload": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["models-dev.refreshed"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["integration.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["integration.connection.updated"] - }, - "properties": { - "type": "object", - "properties": { - "integrationID": { - "type": "string" - } - }, - "required": ["integrationID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["catalog.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.created"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.deleted"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Message" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.removed"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.part.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "part": { - "$ref": "#/components/schemas/Part" - }, - "time": { - "type": "number" - } - }, - "required": ["sessionID", "part", "time"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.part.removed"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } - }, - "required": ["sessionID", "messageID", "partID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.agent.switched"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "agent"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.model.switched"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - } - }, - "required": ["timestamp", "sessionID", "messageID", "model"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.moved"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "subdirectory": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "location"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.prompted"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.prompt.admitted"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.context.updated"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.synthetic"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "command": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "callID", "command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "callID": { - "type": "string" - }, - "output": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "callID", "output"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "snapshot": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "finish": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "snapshot": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.failed"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "error"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.text.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.text.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.text.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "text": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.called"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "input": { - "type": "object" - }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.progress"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.success"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "structured", - "content", - "provider" - ], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.failed"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.retried"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "attempt": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/SessionNextRetry_error" - } - }, - "required": ["timestamp", "sessionID", "attempt", "error"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.started"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.delta"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.ended"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - }, - "text": { - "type": "string" - }, - "recent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.staged"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "revert": { - "$ref": "#/components/schemas/RevertState" - } - }, - "required": ["timestamp", "sessionID", "revert"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.cleared"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["timestamp", "sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.committed"] - }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - } - }, - "required": ["timestamp", "sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["message.part.delta"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "field": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["sessionID", "messageID", "partID", "field", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.diff"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "diff": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["sessionID", "diff"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.error"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/StructuredOutputError" - }, - { - "$ref": "#/components/schemas/ContextOverflowError" - }, - { - "$ref": "#/components/schemas/ContentFilterError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - } - }, - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["installation.updated"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["installation.update-available"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["file.edited"] - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["reference.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.v2.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - } - }, - "required": ["id", "sessionID", "action", "resources"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.v2.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["plugin.added"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": ["id"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["project.directories.updated"] - }, - "properties": { - "type": "object", - "properties": { - "projectID": { - "type": "string" - } - }, - "required": ["projectID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["file.watcher.updated"] - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "type": "string", - "enum": ["add", "change", "unlink"] - } - }, - "required": ["file", "event"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.created"] - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.updated"] - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.exited"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - }, - "exitCode": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["id", "exitCode"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.deleted"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - } - }, - "required": ["id"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Info" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionV2Tool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Answer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.rejected"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["todo.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": ["sessionID", "todos"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["lsp.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["permission.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "type": "string", - "enum": ["once", "always", "reject"] - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["mcp.tools.changed"] - }, - "properties": { - "type": "object", - "properties": { - "server": { - "type": "string" - } - }, - "required": ["server"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["mcp.browser.open.failed"] - }, - "properties": { - "type": "object", - "properties": { - "mcpName": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": ["mcpName", "url"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["command.executed"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "arguments": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["name", "sessionID", "arguments", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["project.updated"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "$ref": "#/components/schemas/ProjectVcs" - }, - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" - }, - "time": { - "$ref": "#/components/schemas/ProjectTime" - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["id", "worktree", "time", "sandboxes"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.status"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "status": { - "$ref": "#/components/schemas/SessionStatus" - } - }, - "required": ["sessionID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.idle"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.asked"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionTool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.replied"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.rejected"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.compacted"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["vcs.branch.updated"] - }, - "properties": { - "type": "object", - "properties": { - "branch": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.ready"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.failed"] - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.status"] - }, - "properties": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - } - }, - "required": ["workspaceID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["worktree.ready"] - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["worktree.failed"] - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["server.connected"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["global.disposed"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - { - "$ref": "#/components/schemas/EventServerInstanceDisposed" - }, - { - "$ref": "#/components/schemas/SyncEventSessionCreated" - }, - { - "$ref": "#/components/schemas/SyncEventSessionUpdated" - }, - { - "$ref": "#/components/schemas/SyncEventSessionDeleted" - }, - { - "$ref": "#/components/schemas/SyncEventMessageUpdated" - }, - { - "$ref": "#/components/schemas/SyncEventMessageRemoved" - }, - { - "$ref": "#/components/schemas/SyncEventMessagePartUpdated" - }, - { - "$ref": "#/components/schemas/SyncEventMessagePartRemoved" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextAgentSwitched" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextModelSwitched" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextMoved" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextPrompted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextPromptAdmitted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextContextUpdated" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextSynthetic" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextShellStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextShellEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextStepStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextStepEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextStepFailed" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextTextStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextTextEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextReasoningStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextReasoningEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolInputStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolInputEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolCalled" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolProgress" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolSuccess" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextToolFailed" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextRetried" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextCompactionStarted" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextRevertStaged" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextRevertCleared" - }, - { - "$ref": "#/components/schemas/SyncEventSessionNextRevertCommitted" - } - ] - } - }, - "required": ["directory", "payload"], - "additionalProperties": false - }, - "LogLevel": { - "type": "string", - "enum": ["DEBUG", "INFO", "WARN", "ERROR"], - "description": "Log level" - }, - "ServerConfig": { - "type": "object", - "properties": { - "port": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "hostname": { - "type": "string" - }, - "mdns": { - "type": "boolean" - }, - "mdnsDomain": { - "type": "string" - }, - "cors": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "description": "Server configuration for opencode serve and web commands" - }, - "PermissionActionConfig": { - "type": "string", - "enum": ["ask", "allow", "deny"] - }, - "PermissionObjectConfig": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/PermissionActionConfig" - } - }, - "PermissionRuleConfig": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - { - "$ref": "#/components/schemas/PermissionObjectConfig" - } - ] - }, - "PermissionConfig": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - { - "type": "object", - "properties": { - "read": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "edit": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "glob": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "grep": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "list": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "bash": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "task": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "external_directory": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "todowrite": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "question": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "webfetch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "websearch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "lsp": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "doom_loop": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "skill": { - "$ref": "#/components/schemas/PermissionRuleConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/PermissionRuleConfig" - } - } - ] - }, - "AgentConfig": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "temperature": { - "type": "number" - }, - "top_p": { - "type": "number" - }, - "prompt": { - "type": "string" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - }, - "disable": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": ["subagent", "primary", "all"] - }, - "hidden": { - "type": "boolean" - }, - "options": { - "type": "object" - }, - "color": { - "anyOf": [ - { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$" - }, - { - "type": "string", - "enum": ["primary", "secondary", "accent", "success", "warning", "error", "info"] - } - ], - "description": "Hex color code (e.g., #FF5733) or theme color (e.g., primary)" - }, - "steps": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "maxSteps": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "permission": { - "$ref": "#/components/schemas/PermissionConfig" - } - }, - "additionalProperties": {} - }, - "ProviderConfig": { - "type": "object", - "properties": { - "api": { - "type": "string" - }, - "name": { - "type": "string" - }, - "env": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "npm": { - "type": "string" - }, - "whitelist": { - "type": "array", - "items": { - "type": "string" - } - }, - "blacklist": { - "type": "array", - "items": { - "type": "string" - } - }, - "options": { - "type": "object", - "properties": { - "apiKey": { - "type": "string" - }, - "baseURL": { - "type": "string" - }, - "enterpriseUrl": { - "type": "string" - }, - "setCacheKey": { - "type": "boolean" - }, - "timeout": { - "anyOf": [ - { - "type": "integer", - "exclusiveMinimum": 0 - }, - { - "type": "boolean", - "enum": [false] - } - ], - "description": "Timeout in milliseconds for full requests to this provider. Set to false to disable timeout." - }, - "headerTimeout": { - "anyOf": [ - { - "type": "integer", - "exclusiveMinimum": 0 - }, - { - "type": "boolean", - "enum": [false] - } - ], - "description": "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout." - }, - "chunkTimeout": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "additionalProperties": {} - }, - "models": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "family": { - "type": "string" - }, - "release_date": { - "type": "string" - }, - "attachment": { - "type": "boolean" - }, - "reasoning": { - "type": "boolean" - }, - "temperature": { - "type": "boolean" - }, - "tool_call": { - "type": "boolean" - }, - "interleaved": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "enum": ["reasoning", "reasoning_content", "reasoning_text"] - }, - { - "type": "string" - }, - { - "type": "object", - "properties": { - "field": { - "anyOf": [ - { - "type": "string", - "enum": ["reasoning", "reasoning_content", "reasoning_text"] - }, - { - "type": "string" - } - ] - } - }, - "required": ["field"], - "additionalProperties": false - } - ] - }, - "cost": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache_read": { - "type": "number" - }, - "cache_write": { - "type": "number" - }, - "context_over_200k": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache_read": { - "type": "number" - }, - "cache_write": { - "type": "number" - } - }, - "required": ["input", "output"], - "additionalProperties": false - } - }, - "required": ["input", "output"], - "additionalProperties": false - }, - "limit": { - "type": "object", - "properties": { - "context": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - } - }, - "required": ["context", "output"], - "additionalProperties": false - }, - "modalities": { - "type": "object", - "properties": { - "input": { - "type": "array", - "items": { - "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] - } - }, - "output": { - "type": "array", - "items": { - "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] - } - } - }, - "additionalProperties": false - }, - "experimental": { - "type": "boolean" - }, - "status": { - "type": "string", - "enum": ["alpha", "beta", "deprecated", "active"] - }, - "provider": { - "type": "object", - "properties": { - "npm": { - "type": "string" - }, - "api": { - "type": "string" - } - }, - "additionalProperties": false - }, - "options": { - "type": "object" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "variants": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "disabled": { - "type": "boolean" - } - }, - "additionalProperties": {} - }, - "description": "Variant-specific configuration" - } - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "McpLocalConfig": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["local"], - "description": "Type of MCP server connection" - }, - "command": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Command and arguments to run the MCP server" - }, - "cwd": { - "type": "string" - }, - "environment": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "timeout": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["type", "command"], - "additionalProperties": false - }, - "McpOAuthConfig": { - "type": "object", - "properties": { - "clientId": { - "type": "string" - }, - "clientSecret": { - "type": "string" - }, - "scope": { - "type": "string" - }, - "callbackPort": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "redirectUri": { - "type": "string" - } - }, - "additionalProperties": false - }, - "McpRemoteConfig": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["remote"], - "description": "Type of MCP server connection" - }, - "url": { - "type": "string", - "description": "URL of the remote MCP server" - }, - "enabled": { - "type": "boolean" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "oauth": { - "anyOf": [ - { - "$ref": "#/components/schemas/McpOAuthConfig" - }, - { - "type": "boolean", - "enum": [false] - } - ], - "description": "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection." - }, - "timeout": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["type", "url"], - "additionalProperties": false - }, - "LayoutConfig": { - "type": "string", - "enum": ["auto", "stretch"], - "description": "@deprecated Always uses stretch layout." - }, - "ImageAttachmentConfig": { - "type": "object", - "properties": { - "auto_resize": { - "type": "boolean" - }, - "max_width": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "max_height": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "max_base64_bytes": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "additionalProperties": false - }, - "AttachmentConfig": { - "type": "object", - "properties": { - "image": { - "$ref": "#/components/schemas/ImageAttachmentConfig" - } - }, - "additionalProperties": false - }, - "Config": { - "type": "object", - "properties": { - "$schema": { - "type": "string" - }, - "shell": { - "type": "string" - }, - "logLevel": { - "$ref": "#/components/schemas/LogLevel" - }, - "server": { - "$ref": "#/components/schemas/ServerConfig" - }, - "command": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "template": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "subtask": { - "type": "boolean" - } - }, - "required": ["template"], - "additionalProperties": false - } - }, - "skills": { - "type": "object", - "properties": { - "paths": { - "type": "array", - "items": { - "type": "string" - } - }, - "urls": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "references": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/components/schemas/ConfigV2ReferenceGit" - }, - { - "$ref": "#/components/schemas/ConfigV2ReferenceLocal" - } - ] - } - }, - "reference": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/components/schemas/ConfigV2ReferenceGit" - }, - { - "$ref": "#/components/schemas/ConfigV2ReferenceLocal" - } - ] - } - }, - "watcher": { - "type": "object", - "properties": { - "ignore": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "snapshot": { - "type": "boolean" - }, - "plugin": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "prefixItems": [ - { - "type": "string" - }, - { - "type": "object" - } - ], - "maxItems": 2, - "minItems": 2 - } - ] - } - }, - "share": { - "type": "string", - "enum": ["manual", "auto", "disabled"] - }, - "autoshare": { - "type": "boolean" - }, - "autoupdate": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "enum": ["notify"] - } - ], - "description": "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications" - }, - "disabled_providers": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled_providers": { - "type": "array", - "items": { - "type": "string" - } - }, - "model": { - "type": "string" - }, - "small_model": { - "type": "string" - }, - "default_agent": { - "type": "string" - }, - "subagent_depth": { - "type": "integer", - "minimum": 0 - }, - "username": { - "type": "string" - }, - "mode": { - "type": "object", - "properties": { - "build": { - "$ref": "#/components/schemas/AgentConfig" - }, - "plan": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "agent": { - "type": "object", - "properties": { - "plan": { - "$ref": "#/components/schemas/AgentConfig" - }, - "build": { - "$ref": "#/components/schemas/AgentConfig" - }, - "general": { - "$ref": "#/components/schemas/AgentConfig" - }, - "explore": { - "$ref": "#/components/schemas/AgentConfig" - }, - "title": { - "$ref": "#/components/schemas/AgentConfig" - }, - "summary": { - "$ref": "#/components/schemas/AgentConfig" - }, - "compaction": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "provider": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ProviderConfig" - } - }, - "mcp": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/components/schemas/McpLocalConfig" - }, - { - "$ref": "#/components/schemas/McpRemoteConfig" - }, - { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - }, - "required": ["enabled"], - "additionalProperties": false - } - ] - } - }, - "formatter": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "disabled": { - "type": "boolean" - }, - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "environment": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - } - } - ], - "description": "Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides." - }, - "lsp": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "object", - "properties": { - "disabled": { - "type": "boolean", - "enum": [true] - } - }, - "required": ["disabled"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - } - }, - "disabled": { - "type": "boolean" - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "initialization": { - "type": "object" - } - }, - "required": ["command"], - "additionalProperties": false - } - ] - } - } - ], - "description": "Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides." - }, - "instructions": { - "type": "array", - "items": { - "type": "string" - } - }, - "layout": { - "$ref": "#/components/schemas/LayoutConfig" - }, - "permission": { - "$ref": "#/components/schemas/PermissionConfig" - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - }, - "attachment": { - "$ref": "#/components/schemas/AttachmentConfig" - }, - "enterprise": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - "tool_output": { - "type": "object", - "properties": { - "max_lines": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "max_bytes": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "additionalProperties": false - }, - "compaction": { - "type": "object", - "properties": { - "auto": { - "type": "boolean" - }, - "prune": { - "type": "boolean" - }, - "tail_turns": { - "type": "integer", - "minimum": 0 - }, - "preserve_recent_tokens": { - "type": "integer", - "minimum": 0 - }, - "reserved": { - "type": "integer", - "minimum": 0 - } - }, - "additionalProperties": false - }, - "experimental": { - "type": "object", - "properties": { - "disable_paste_summary": { - "type": "boolean" - }, - "batch_tool": { - "type": "boolean" - }, - "openTelemetry": { - "type": "boolean" - }, - "primary_tools": { - "type": "array", - "items": { - "type": "string" - } - }, - "continue_loop_on_deny": { - "type": "boolean" - }, - "mcp_timeout": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "policies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ConfigV2ExperimentalPolicy" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "Model": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "api": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "npm": { - "type": "string" - } - }, - "required": ["id", "url", "npm"], - "additionalProperties": false - }, - "name": { - "type": "string" - }, - "family": { - "type": "string" - }, - "capabilities": { - "type": "object", - "properties": { - "temperature": { - "type": "boolean" - }, - "reasoning": { - "type": "boolean" - }, - "attachment": { - "type": "boolean" - }, - "toolcall": { - "type": "boolean" - }, - "input": { - "type": "object", - "properties": { - "text": { - "type": "boolean" - }, - "audio": { - "type": "boolean" - }, - "image": { - "type": "boolean" - }, - "video": { - "type": "boolean" - }, - "pdf": { - "type": "boolean" - } - }, - "required": ["text", "audio", "image", "video", "pdf"], - "additionalProperties": false - }, - "output": { - "type": "object", - "properties": { - "text": { - "type": "boolean" - }, - "audio": { - "type": "boolean" - }, - "image": { - "type": "boolean" - }, - "video": { - "type": "boolean" - }, - "pdf": { - "type": "boolean" - } - }, - "required": ["text", "audio", "image", "video", "pdf"], - "additionalProperties": false - }, - "interleaved": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "object", - "properties": { - "field": { - "anyOf": [ - { - "type": "string", - "enum": ["reasoning", "reasoning_content", "reasoning_text"] - }, - { - "type": "string" - } - ] - } - }, - "required": ["field"], - "additionalProperties": false - } - ] - } - }, - "required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output", "interleaved"], - "additionalProperties": false - }, - "cost": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - }, - "tiers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - }, - "tier": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["context"] - }, - "size": { - "type": "number" - } - }, - "required": ["type", "size"], - "additionalProperties": false - } - }, - "required": ["input", "output", "cache", "tier"], - "additionalProperties": false - } - }, - "experimentalOver200K": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "cache"], - "additionalProperties": false - } - }, - "required": ["input", "output", "cache"], - "additionalProperties": false - }, - "limit": { - "type": "object", - "properties": { - "context": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - } - }, - "required": ["context", "output"], - "additionalProperties": false - }, - "status": { - "type": "string", - "enum": ["alpha", "beta", "deprecated", "active"] - }, - "options": { - "type": "object" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "release_date": { - "type": "string" - }, - "variants": { - "type": "object", - "additionalProperties": { - "type": "object" - } - } - }, - "required": [ - "id", - "providerID", - "api", - "name", - "capabilities", - "cost", - "limit", - "status", - "options", - "headers", - "release_date" - ], - "additionalProperties": false - }, - "Provider": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "source": { - "type": "string", - "enum": ["env", "config", "custom", "api"] - }, - "env": { - "type": "array", - "items": { - "type": "string" - } - }, - "key": { - "type": "string" - }, - "options": { - "type": "object" - }, - "models": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Model" - } - } - }, - "required": ["id", "name", "source", "env", "options", "models"], - "additionalProperties": false - }, - "ExperimentalCapabilities": { - "type": "object", - "properties": { - "backgroundSubagents": { - "type": "boolean" - } - }, - "required": ["backgroundSubagents"], - "additionalProperties": false - }, - "ConsoleState": { - "type": "object", - "properties": { - "consoleManagedProviders": { - "type": "array", - "items": { - "type": "string" - } - }, - "activeOrgName": { - "type": "string" - }, - "switchableOrgCount": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["consoleManagedProviders", "switchableOrgCount"], - "additionalProperties": false - }, - "effect_HttpApiError_InternalServerError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["InternalServerError"] - } - }, - "required": ["_tag"], - "additionalProperties": false - }, - "ToolListItem": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "description": { - "type": "string" - }, - "parameters": {} - }, - "required": ["id", "description", "parameters"], - "additionalProperties": false - }, - "ToolList": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ToolListItem" - } - }, - "ToolIDs": { - "type": "array", - "items": { - "type": "string" - } - }, - "WorktreeError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": [ - "WorktreeNotGitError", - "WorktreeNameGenerationFailedError", - "WorktreeCreateFailedError", - "WorktreeStartCommandFailedError", - "WorktreeRemoveFailedError", - "WorktreeResetFailedError", - "WorktreeListFailedError" - ] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "WorktreeCreateInput": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "startCommand": { - "type": "string", - "description": "Additional startup script to run after the project's start command" - } - }, - "additionalProperties": false - }, - "Worktree": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - }, - "directory": { - "type": "string" - } - }, - "required": ["name", "directory"], - "additionalProperties": false - }, - "WorktreeRemoveInput": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - }, - "WorktreeResetInput": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - }, - "ProjectSummary": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "worktree": { - "type": "string" - } - }, - "required": ["id", "worktree"], - "additionalProperties": false - }, - "GlobalSession": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^ses" - }, - "slug": { - "type": "string" - }, - "projectID": { - "type": "string" - }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" - }, - "directory": { - "type": "string" - }, - "path": { - "type": "string" - }, - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "summary": { - "type": "object", - "properties": { - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "files": { - "type": "number" - }, - "diffs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } - } - }, - "required": ["additions", "deletions", "files"], - "additionalProperties": false - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "share": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"], - "additionalProperties": false - }, - "title": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["id", "providerID"], - "additionalProperties": false - }, - "version": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "minimum": 0 - }, - "updated": { - "type": "integer", - "minimum": 0 - }, - "compacting": { - "type": "integer", - "minimum": 0 - }, - "archived": { - "type": "number" - } - }, - "required": ["created", "updated"], - "additionalProperties": false - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "revert": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "snapshot": { - "type": "string" - }, - "diff": { - "type": "string" - } - }, - "required": ["messageID"], - "additionalProperties": false - }, - "project": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProjectSummary" - }, - { - "type": "null" - } - ] - } - }, - "required": ["id", "slug", "projectID", "directory", "title", "version", "time", "project"], - "additionalProperties": false - }, - "McpResource": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "uri": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "client": { - "type": "string" - } - }, - "required": ["name", "uri", "client"], - "additionalProperties": false - }, - "Symbol": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "kind": { - "type": "integer", - "minimum": 0 - }, - "location": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "range": { - "$ref": "#/components/schemas/Range" - } - }, - "required": ["uri", "range"], - "additionalProperties": false - } - }, - "required": ["name", "kind", "location"], - "additionalProperties": false - }, - "FileNode": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "absolute": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["file", "directory"] - }, - "ignored": { - "type": "boolean" - } - }, - "required": ["name", "path", "absolute", "type", "ignored"], - "additionalProperties": false - }, - "FileContent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text", "binary"] - }, - "content": { - "type": "string" - }, - "diff": { - "type": "string" - }, - "patch": { - "type": "object", - "properties": { - "oldFileName": { - "type": "string" - }, - "newFileName": { - "type": "string" - }, - "oldHeader": { - "type": "string" - }, - "newHeader": { - "type": "string" - }, - "hunks": { - "type": "array", - "items": { - "type": "object", - "properties": { - "oldStart": { - "type": "integer", - "minimum": 0 - }, - "oldLines": { - "type": "integer", - "minimum": 0 - }, - "newStart": { - "type": "integer", - "minimum": 0 - }, - "newLines": { - "type": "integer", - "minimum": 0 - }, - "lines": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["oldStart", "oldLines", "newStart", "newLines", "lines"], - "additionalProperties": false - } - }, - "index": { - "type": "string" - } - }, - "required": ["oldFileName", "newFileName", "hunks"], - "additionalProperties": false - }, - "encoding": { - "type": "string", - "enum": ["base64"] - }, - "mimeType": { - "type": "string" - } - }, - "required": ["type", "content"], - "additionalProperties": false - }, - "File": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "added": { - "type": "integer", - "minimum": 0 - }, - "removed": { - "type": "integer", - "minimum": 0 - }, - "status": { - "type": "string", - "enum": ["added", "deleted", "modified"] - } - }, - "required": ["path", "added", "removed", "status"], - "additionalProperties": false - }, - "Path": { - "type": "object", - "properties": { - "home": { - "type": "string" - }, - "state": { - "type": "string" - }, - "config": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "directory": { - "type": "string" - } - }, - "required": ["home", "state", "config", "worktree", "directory"], - "additionalProperties": false - }, - "VcsInfo": { - "type": "object", - "properties": { - "branch": { - "type": "string" - }, - "default_branch": { - "type": "string" - } - }, - "additionalProperties": false - }, - "VcsFileStatus": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "status": { - "type": "string", - "enum": ["added", "deleted", "modified"] - } - }, - "required": ["file", "additions", "deletions", "status"], - "additionalProperties": false - }, - "VcsFileDiff": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "patch": { - "type": "string" - }, - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "status": { - "type": "string", - "enum": ["added", "deleted", "modified"] - } - }, - "required": ["file", "additions", "deletions"], - "additionalProperties": false - }, - "VcsApplyError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["VcsApplyError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "reason": { - "type": "string", - "enum": ["non-git", "not-clean"] - } - }, - "required": ["message", "reason"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "Command": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "source": { - "type": "string", - "enum": ["command", "mcp", "skill"] - }, - "template": { - "type": "string" - }, - "subtask": { - "type": "boolean" - }, - "hints": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["name", "template", "hints"], - "additionalProperties": false - }, - "Agent": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": ["subagent", "primary", "all"] - }, - "native": { - "type": "boolean" - }, - "hidden": { - "type": "boolean" - }, - "topP": { - "type": "number" - }, - "temperature": { - "type": "number" - }, - "color": { - "type": "string" - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "model": { - "type": "object", - "properties": { - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - } - }, - "required": ["modelID", "providerID"], - "additionalProperties": false - }, - "variant": { - "type": "string" - }, - "prompt": { - "type": "string" - }, - "options": { - "type": "object" - }, - "steps": { - "type": "number" - } - }, - "required": ["name", "mode", "permission", "options"], - "additionalProperties": false - }, - "LSPStatus": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "root": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["connected", "error"] - } - }, - "required": ["id", "name", "root", "status"], - "additionalProperties": false - }, - "FormatterStatus": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - } - }, - "required": ["name", "extensions", "enabled"], - "additionalProperties": false - }, - "MCPStatusConnected": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["connected"] - } - }, - "required": ["status"], - "additionalProperties": false - }, - "MCPStatusDisabled": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["disabled"] - } - }, - "required": ["status"], - "additionalProperties": false - }, - "MCPStatusFailed": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["failed"] - }, - "error": { - "type": "string" - } - }, - "required": ["status", "error"], - "additionalProperties": false - }, - "MCPStatusNeedsAuth": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["needs_auth"] - } - }, - "required": ["status"], - "additionalProperties": false - }, - "MCPStatusNeedsClientRegistration": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["needs_client_registration"] - }, - "error": { - "type": "string" - } - }, - "required": ["status", "error"], - "additionalProperties": false - }, - "MCPStatus": { - "anyOf": [ - { - "$ref": "#/components/schemas/MCPStatusConnected" - }, - { - "$ref": "#/components/schemas/MCPStatusDisabled" - }, - { - "$ref": "#/components/schemas/MCPStatusFailed" - }, - { - "$ref": "#/components/schemas/MCPStatusNeedsAuth" - }, - { - "$ref": "#/components/schemas/MCPStatusNeedsClientRegistration" - } - ] - }, - "McpUnsupportedOAuthError": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - }, - "required": ["error"], - "additionalProperties": false - }, - "McpServerNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["McpServerNotFoundError"] - }, - "name": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "name", "message"], - "additionalProperties": false - }, - "Project": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "$ref": "#/components/schemas/ProjectVcs" - }, - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" - }, - "time": { - "$ref": "#/components/schemas/ProjectTime" - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["id", "worktree", "time", "sandboxes"], - "additionalProperties": false - }, - "ProjectNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ProjectNotFoundError"] - }, - "projectID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "projectID", "message"], - "additionalProperties": false - }, - "PtyNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["PtyNotFoundError"] - }, - "ptyID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "ptyID", "message"], - "additionalProperties": false - }, - "PtyForbiddenError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["PtyForbiddenError"] - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "QuestionRequest": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionTool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - }, - "QuestionNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["QuestionNotFoundError"] - }, - "requestID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "requestID", "message"], - "additionalProperties": false - }, - "PermissionRequest": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], - "additionalProperties": false - }, - "PermissionNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["PermissionNotFoundError"] - }, - "requestID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "requestID", "message"], - "additionalProperties": false - }, - "ProviderAuthMethod": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["oauth", "api"] - }, - "label": { - "type": "string" - }, - "prompts": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - }, - "key": { - "type": "string" - }, - "message": { - "type": "string" - }, - "placeholder": { - "type": "string" - }, - "when": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "op": { - "type": "string", - "enum": ["eq", "neq"] - }, - "value": { - "type": "string" - } - }, - "required": ["key", "op", "value"], - "additionalProperties": false - } - }, - "required": ["type", "key", "message"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["select"] - }, - "key": { - "type": "string" - }, - "message": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "value": { - "type": "string" - }, - "hint": { - "type": "string" - } - }, - "required": ["label", "value"], - "additionalProperties": false - } - }, - "when": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "op": { - "type": "string", - "enum": ["eq", "neq"] - }, - "value": { - "type": "string" - } - }, - "required": ["key", "op", "value"], - "additionalProperties": false - } - }, - "required": ["type", "key", "message", "options"], - "additionalProperties": false - } - ] - } - } - }, - "required": ["type", "label"], - "additionalProperties": false - }, - "ProviderAuthAuthorization": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "method": { - "type": "string", - "enum": ["auto", "code"] - }, - "instructions": { - "type": "string" - } - }, - "required": ["url", "method", "instructions"], - "additionalProperties": false - }, - "ProviderAuthError1": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": [ - "BadRequest", - "ProviderAuthOauthMissing", - "ProviderAuthOauthCodeMissing", - "ProviderAuthOauthCallbackFailed", - "ProviderAuthValidationFailed" - ] - }, - "data": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "field": { - "type": "string" - }, - "message": { - "type": "string" - }, - "kind": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "NotFoundError": { - "type": "object", - "required": ["name", "data"], - "properties": { - "name": { - "type": "string", - "enum": ["NotFoundError"] - }, - "data": { - "type": "object", - "required": ["message"], - "properties": { - "message": { - "type": "string" - } - } - } - } - }, - "TextPartInput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["text"] - }, - "text": { - "type": "string" - }, - "synthetic": { - "type": "boolean" - }, - "ignored": { - "type": "boolean" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["start"], - "additionalProperties": false - }, - "metadata": { - "type": "object" - } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - "FilePartInput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["file"] - }, - "mime": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/FilePartSource" - } - }, - "required": ["type", "mime", "url"], - "additionalProperties": false - }, - "AgentPartInput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["agent"] - }, - "name": { - "type": "string" - }, - "source": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["value", "start", "end"], - "additionalProperties": false - } - }, - "required": ["type", "name"], - "additionalProperties": false - }, - "SubtaskPartInput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^prt" - }, - "type": { - "type": "string", - "enum": ["subtask"] - }, - "prompt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": ["providerID", "modelID"], - "additionalProperties": false - }, - "command": { - "type": "string" - } - }, - "required": ["type", "prompt", "description", "agent"], - "additionalProperties": false - }, - "SessionBusyError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["SessionBusyError"] - }, - "sessionID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "sessionID", "message"], - "additionalProperties": false - }, - "EventTuiPromptAppend": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["type", "properties"], - "additionalProperties": false - }, - "EventTuiCommandExecute": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"], - "additionalProperties": false - } - }, - "required": ["type", "properties"], - "additionalProperties": false - }, - "EventTuiToastShow": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["type", "properties"], - "additionalProperties": false - }, - "EventTuiSessionSelect": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["type", "properties"], - "additionalProperties": false - }, - "Workspace": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^wrk" - }, - "type": { - "type": "string" - }, - "name": { - "type": "string" - }, - "branch": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "extra": { - "anyOf": [ - {}, - { - "type": "null" - } - ] - }, - "projectID": { - "type": "string" - }, - "timeUsed": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["id", "type", "name", "projectID", "timeUsed"], - "additionalProperties": false - }, - "WorkspaceCreateError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["WorkspaceCreateError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "WorkspaceWarpError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["WorkspaceWarpError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "UnauthorizedError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["UnauthorizedError"] - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "SessionsResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionV2Info" - } - }, - "cursor": { - "type": "object", - "properties": { - "previous": { - "type": "string" - }, - "next": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["data", "cursor"], - "additionalProperties": false - }, - "InvalidCursorError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["InvalidCursorError"] - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "SessionActive": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["running"] - } - }, - "required": ["type"], - "additionalProperties": false - }, - "SessionNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["SessionNotFoundError"] - }, - "sessionID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "sessionID", "message"], - "additionalProperties": false - }, - "PromptInput": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptInputFileAttachment" - } - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptAgentAttachment" - } - } - }, - "required": ["text"], - "additionalProperties": false - }, - "ConflictError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ConflictError"] - }, - "message": { - "type": "string" - }, - "resource": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "ServiceUnavailableError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ServiceUnavailableError"] - }, - "message": { - "type": "string" - }, - "service": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "MessageNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["MessageNotFoundError"] - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "sessionID", "messageID", "message"], - "additionalProperties": false - }, - "UnknownError1": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["UnknownError"] - }, - "message": { - "type": "string" - }, - "ref": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "SessionDurableEvent": { - "oneOf": [ - { - "$ref": "#/components/schemas/SessionNextAgentSwitched" - }, - { - "$ref": "#/components/schemas/SessionNextModelSwitched" - }, - { - "$ref": "#/components/schemas/SessionNextMoved" - }, - { - "$ref": "#/components/schemas/SessionNextPrompted" - }, - { - "$ref": "#/components/schemas/SessionNextPromptAdmitted" - }, - { - "$ref": "#/components/schemas/SessionNextContextUpdated" - }, - { - "$ref": "#/components/schemas/SessionNextSynthetic" - }, - { - "$ref": "#/components/schemas/SessionNextShellStarted" - }, - { - "$ref": "#/components/schemas/SessionNextShellEnded" - }, - { - "$ref": "#/components/schemas/SessionNextStepStarted" - }, - { - "$ref": "#/components/schemas/SessionNextStepEnded" - }, - { - "$ref": "#/components/schemas/SessionNextStepFailed" - }, - { - "$ref": "#/components/schemas/SessionNextTextStarted" - }, - { - "$ref": "#/components/schemas/SessionNextTextEnded" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputStarted" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputEnded" - }, - { - "$ref": "#/components/schemas/SessionNextToolCalled" - }, - { - "$ref": "#/components/schemas/SessionNextToolProgress" - }, - { - "$ref": "#/components/schemas/SessionNextToolSuccess" - }, - { - "$ref": "#/components/schemas/SessionNextToolFailed" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningStarted" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningEnded" - }, - { - "$ref": "#/components/schemas/SessionNextRetried" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionStarted" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/SessionNextRevertStaged" - }, - { - "$ref": "#/components/schemas/SessionNextRevertCleared" - }, - { - "$ref": "#/components/schemas/SessionNextRevertCommitted" - } - ] - }, - "SessionHistory": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionDurableEvent" - } - }, - "hasMore": { - "type": "boolean" - } - }, - "required": ["data", "hasMore"], - "additionalProperties": false - }, - "SessionDurableEventStream": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/SessionDurableEvent" - }, - "contentMediaType": "application/json" - }, - "SessionMessagesResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionMessage" - } - }, - "cursor": { - "type": "object", - "properties": { - "previous": { - "type": "string" - }, - "next": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "required": ["data", "cursor"], - "additionalProperties": false - }, - "ProviderNotFoundError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ProviderNotFoundError"] - }, - "providerID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "providerID", "message"], - "additionalProperties": false - }, - "OutputFormat1": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - } - }, - "required": ["type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["json_schema"] - }, - "schema": { - "$ref": "#/components/schemas/JSONSchema" - }, - "retryCount": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["type", "schema"], - "additionalProperties": false - } - ] - }, - "session.status": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.status"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "status": { - "$ref": "#/components/schemas/SessionStatus" - } - }, - "required": ["sessionID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "question.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["question.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "question.rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["question.rejected"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "V2Event": { - "anyOf": [ - { - "$ref": "#/components/schemas/Models-devRefreshed" - }, - { - "$ref": "#/components/schemas/IntegrationUpdated" - }, - { - "$ref": "#/components/schemas/IntegrationConnectionUpdated" - }, - { - "$ref": "#/components/schemas/CatalogUpdated" - }, - { - "$ref": "#/components/schemas/SessionCreated" - }, - { - "$ref": "#/components/schemas/SessionUpdated" - }, - { - "$ref": "#/components/schemas/SessionDeleted" - }, - { - "$ref": "#/components/schemas/MessageUpdated" - }, - { - "$ref": "#/components/schemas/MessageRemoved" - }, - { - "$ref": "#/components/schemas/MessagePartUpdated" - }, - { - "$ref": "#/components/schemas/MessagePartRemoved" - }, - { - "$ref": "#/components/schemas/SessionNextAgentSwitched" - }, - { - "$ref": "#/components/schemas/SessionNextModelSwitched" - }, - { - "$ref": "#/components/schemas/SessionNextMoved" - }, - { - "$ref": "#/components/schemas/SessionNextPrompted" - }, - { - "$ref": "#/components/schemas/SessionNextPromptAdmitted" - }, - { - "$ref": "#/components/schemas/SessionNextContextUpdated" - }, - { - "$ref": "#/components/schemas/SessionNextSynthetic" - }, - { - "$ref": "#/components/schemas/SessionNextShellStarted" - }, - { - "$ref": "#/components/schemas/SessionNextShellEnded" - }, - { - "$ref": "#/components/schemas/SessionNextStepStarted" - }, - { - "$ref": "#/components/schemas/SessionNextStepEnded" - }, - { - "$ref": "#/components/schemas/SessionNextStepFailed" - }, - { - "$ref": "#/components/schemas/SessionNextTextStarted" - }, - { - "$ref": "#/components/schemas/SessionNextTextDelta" - }, - { - "$ref": "#/components/schemas/SessionNextTextEnded" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningStarted" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningDelta" - }, - { - "$ref": "#/components/schemas/SessionNextReasoningEnded" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputStarted" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputDelta" - }, - { - "$ref": "#/components/schemas/SessionNextToolInputEnded" - }, - { - "$ref": "#/components/schemas/SessionNextToolCalled" - }, - { - "$ref": "#/components/schemas/SessionNextToolProgress" - }, - { - "$ref": "#/components/schemas/SessionNextToolSuccess" - }, - { - "$ref": "#/components/schemas/SessionNextToolFailed" - }, - { - "$ref": "#/components/schemas/SessionNextRetried" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionStarted" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionDelta" - }, - { - "$ref": "#/components/schemas/SessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/SessionNextRevertStaged" - }, - { - "$ref": "#/components/schemas/SessionNextRevertCleared" - }, - { - "$ref": "#/components/schemas/SessionNextRevertCommitted" - }, - { - "$ref": "#/components/schemas/MessagePartDelta" - }, - { - "$ref": "#/components/schemas/SessionDiff" - }, - { - "$ref": "#/components/schemas/SessionError" - }, - { - "$ref": "#/components/schemas/InstallationUpdated" - }, - { - "$ref": "#/components/schemas/InstallationUpdate-available" - }, - { - "$ref": "#/components/schemas/FileEdited" - }, - { - "$ref": "#/components/schemas/ReferenceUpdated" - }, - { - "$ref": "#/components/schemas/PermissionV2Asked" - }, - { - "$ref": "#/components/schemas/PermissionV2Replied" - }, - { - "$ref": "#/components/schemas/PluginAdded" - }, - { - "$ref": "#/components/schemas/ProjectDirectoriesUpdated" - }, - { - "$ref": "#/components/schemas/FileWatcherUpdated" - }, - { - "$ref": "#/components/schemas/PtyCreated" - }, - { - "$ref": "#/components/schemas/PtyUpdated" - }, - { - "$ref": "#/components/schemas/PtyExited" - }, - { - "$ref": "#/components/schemas/PtyDeleted" - }, - { - "$ref": "#/components/schemas/QuestionV2Asked" - }, - { - "$ref": "#/components/schemas/QuestionV2Replied" - }, - { - "$ref": "#/components/schemas/QuestionV2Rejected" - }, - { - "$ref": "#/components/schemas/TodoUpdated" - }, - { - "$ref": "#/components/schemas/LspUpdated" - }, - { - "$ref": "#/components/schemas/PermissionAsked" - }, - { - "$ref": "#/components/schemas/PermissionReplied" - }, - { - "$ref": "#/components/schemas/TuiPromptAppend" - }, - { - "$ref": "#/components/schemas/TuiCommandExecute" - }, - { - "$ref": "#/components/schemas/TuiToastShow" - }, - { - "$ref": "#/components/schemas/TuiSessionSelect" - }, - { - "$ref": "#/components/schemas/McpToolsChanged" - }, - { - "$ref": "#/components/schemas/McpBrowserOpenFailed" - }, - { - "$ref": "#/components/schemas/CommandExecuted" - }, - { - "$ref": "#/components/schemas/ProjectUpdated" - }, - { - "$ref": "#/components/schemas/session.status" - }, - { - "$ref": "#/components/schemas/SessionIdle" - }, - { - "$ref": "#/components/schemas/QuestionAsked" - }, - { - "$ref": "#/components/schemas/question.replied" - }, - { - "$ref": "#/components/schemas/question.rejected" - }, - { - "$ref": "#/components/schemas/SessionCompacted" - }, - { - "$ref": "#/components/schemas/VcsBranchUpdated" - }, - { - "$ref": "#/components/schemas/WorkspaceReady" - }, - { - "$ref": "#/components/schemas/WorkspaceFailed" - }, - { - "$ref": "#/components/schemas/WorkspaceStatus" - }, - { - "$ref": "#/components/schemas/WorktreeReady" - }, - { - "$ref": "#/components/schemas/WorktreeFailed" - }, - { - "$ref": "#/components/schemas/ServerConnected" - }, - { - "$ref": "#/components/schemas/GlobalDisposed" - } - ] - }, - "V2EventStream": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/V2Event" - }, - "contentMediaType": "application/json" - }, - "ForbiddenError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["ForbiddenError"] - }, - "message": { - "type": "string" - } - }, - "required": ["_tag", "message"], - "additionalProperties": false - }, - "ProjectCopyError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "enum": ["ProjectCopyError"] - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "forceRequired": { - "type": "boolean" - } - }, - "required": ["message"], - "additionalProperties": false - } - }, - "required": ["name", "data"], - "additionalProperties": false - }, - "effect_HttpApiError_Forbidden": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Forbidden"] - } - }, - "required": ["_tag"], - "additionalProperties": false - }, - "Event.tui.prompt.append": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.command.execute": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.toast.show": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.session.select": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "CredentialValue": { - "anyOf": [ - { - "$ref": "#/components/schemas/CredentialOAuth" - }, - { - "$ref": "#/components/schemas/CredentialKey" - } - ] - }, - "IntegrationInputs": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "IntegrationMethod": { - "anyOf": [ - { - "$ref": "#/components/schemas/IntegrationOAuthMethod" - }, - { - "$ref": "#/components/schemas/IntegrationKeyMethod" - }, - { - "$ref": "#/components/schemas/IntegrationEnvMethod" - } - ] - }, - "IntegrationRef": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["id", "name"], - "additionalProperties": false - }, - "SkillV2Source": { - "anyOf": [ - { - "$ref": "#/components/schemas/SkillV2DirectorySource" - }, - { - "$ref": "#/components/schemas/SkillV2UrlSource" - }, - { - "$ref": "#/components/schemas/SkillV2EmbeddedSource" - } - ] - }, - "MoveSessionDestination": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - }, - "ModelRef": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "variant": { - "type": "string" - } - }, - "required": ["id", "providerID"], + "required": ["settings", "headers", "body"], "additionalProperties": false }, - "LocationRef": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" - } - }, - "required": ["directory"], - "additionalProperties": false + "Agent.Color": { + "type": "string" }, - "PromptSource": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - }, - "text": { - "type": "string" - } - }, - "required": ["start", "end", "text"], - "additionalProperties": false + "Permission.Effect": { + "type": "string", + "enum": ["allow", "deny", "ask"] }, - "PromptFileAttachment": { + "Permission.Rule": { "type": "object", "properties": { - "uri": { - "type": "string" - }, - "mime": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { + "action": { "type": "string" }, - "source": { - "$ref": "#/components/schemas/PromptSource" - } - }, - "required": ["uri", "mime"], - "additionalProperties": false - }, - "PromptAgentAttachment": { - "type": "object", - "properties": { - "name": { + "resource": { "type": "string" }, - "source": { - "$ref": "#/components/schemas/PromptSource" - } - }, - "required": ["name"], - "additionalProperties": false - }, - "SessionErrorUnknown": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["unknown"] - }, - "message": { - "type": "string" + "effect": { + "$ref": "#/components/schemas/Permission.Effect" } }, - "required": ["type", "message"], + "required": ["action", "resource", "effect"], "additionalProperties": false }, - "LLMProviderMetadata": { - "type": "object", - "additionalProperties": { - "type": "object" + "Permission.Ruleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Permission.Rule" } }, - "ToolTextContent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] - }, - "text": { - "type": "string" - } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - "ToolFileContent": { + "Agent.Info": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["file"] - }, - "uri": { - "type": "string" - }, - "mime": { + "id": { "type": "string" }, "name": { "type": "string" - } - }, - "required": ["type", "uri", "mime"], - "additionalProperties": false - }, - "LLMToolContent": { - "anyOf": [ - { - "$ref": "#/components/schemas/ToolTextContent" - }, - { - "$ref": "#/components/schemas/ToolFileContent" - } - ] - }, - "SessionNextRetry_error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "statusCode": { - "type": "number" - }, - "isRetryable": { - "type": "boolean" - }, - "responseHeaders": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "responseBody": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["message", "isRetryable"], - "additionalProperties": false - }, - "FileDiff": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["added", "modified", "deleted"] - }, - "additions": { - "type": "integer", - "minimum": 0 - }, - "deletions": { - "type": "integer", - "minimum": 0 }, - "patch": { - "type": "string" - } - }, - "required": ["path", "status", "additions", "deletions", "patch"], - "additionalProperties": false - }, - "RevertState": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg_" + "model": { + "$ref": "#/components/schemas/Model.Ref" }, - "partID": { - "type": "string" + "request": { + "$ref": "#/components/schemas/Provider.Request" }, - "snapshot": { + "system": { "type": "string" }, - "diff": { + "description": { "type": "string" }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileDiff" - } - } - }, - "required": ["messageID"], - "additionalProperties": false - }, - "PermissionV2Source": { - "type": "object", - "properties": { - "type": { + "mode": { "type": "string", - "enum": ["tool"] + "enum": ["subagent", "primary", "all"] }, - "messageID": { - "type": "string" + "hidden": { + "type": "boolean" }, - "callID": { - "type": "string" - } - }, - "required": ["type", "messageID", "callID"], - "additionalProperties": false - }, - "PermissionV2Reply": { - "type": "string", - "enum": ["once", "always", "reject"] - }, - "QuestionV2Option": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" + "color": { + "$ref": "#/components/schemas/Agent.Color" }, - "description": { - "type": "string", - "description": "Explanation of choice" + "steps": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "permissions": { + "$ref": "#/components/schemas/Permission.Ruleset" } }, - "required": ["label", "description"], + "required": ["id", "name", "request", "mode", "hidden", "permissions"], "additionalProperties": false }, - "QuestionV2Info": { + "AgentNotFoundError": { "type": "object", "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { + "_tag": { "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Option" - }, - "description": "Available choices" + "enum": ["AgentNotFoundError"] }, - "multiple": { - "type": "boolean" + "agentID": { + "type": "string" }, - "custom": { - "type": "boolean" + "message": { + "type": "string" } }, - "required": ["question", "header", "options"], + "required": ["_tag", "agentID", "message"], "additionalProperties": false }, - "QuestionV2Tool": { + "Plugin.Info": { "type": "object", "properties": { - "messageID": { - "type": "string" - }, - "callID": { + "id": { "type": "string" } }, - "required": ["messageID", "callID"], + "required": ["id"], "additionalProperties": false }, - "QuestionV2Answer": { - "type": "array", - "items": { - "type": "string" - } + "Session.ForkBoundary": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["before"] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": ["type", "messageID"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["through"] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": ["type", "messageID"], + "additionalProperties": false + } + ] }, - "ProjectVcs": { - "type": "string", - "enum": ["git"] + "Money.USD": { + "type": "number" }, - "ProjectIcon": { + "TokenUsage.Info": { "type": "object", "properties": { - "url": { - "type": "string" + "input": { + "type": "number" }, - "override": { - "type": "string" + "output": { + "type": "number" }, - "color": { - "type": "string" + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"], + "additionalProperties": false } }, + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false }, - "ProjectCommands": { + "Location.Ref": { "type": "object", "properties": { - "start": { + "directory": { + "type": "string" + }, + "workspaceID": { "type": "string", - "description": "Startup script to run when creating a new workspace (worktree)" + "allOf": [ + { + "pattern": "^wrk" + } + ] } }, + "required": ["directory"], "additionalProperties": false }, - "ProjectTime": { + "FileDiff.Info": { "type": "object", "properties": { - "created": { - "type": "integer", - "minimum": 0 + "file": { + "type": "string" }, - "updated": { + "patch": { + "type": "string" + }, + "additions": { "type": "integer", - "minimum": 0 + "allOf": [ + { + "minimum": 0 + } + ] }, - "initialized": { + "deletions": { "type": "integer", - "minimum": 0 - } - }, - "required": ["created", "updated"], - "additionalProperties": false - }, - "EventServerInstanceDisposed": { - "type": "object", - "properties": { - "id": { - "type": "string" + "allOf": [ + { + "minimum": 0 + } + ] }, - "type": { + "status": { "type": "string", - "enum": ["server.instance.disposed"] - }, - "properties": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false + "enum": ["added", "deleted", "modified"] } }, - "required": ["id", "type", "properties"], + "required": ["file", "patch", "additions", "deletions", "status"], "additionalProperties": false }, - "SyncEventSessionCreated": { + "Session.Revert": { "type": "object", "properties": { - "type": { + "messageID": { "type": "string", - "enum": ["sync"] + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "id": { - "type": "string", - "pattern": "^evt_" + "partID": { + "type": "string" }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.created.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + "snapshot": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff.Info" + } } }, - "required": ["type", "id", "syncEvent"], + "required": ["messageID"], "additionalProperties": false }, - "SyncEventSessionUpdated": { + "Session.Info": { "type": "object", "properties": { - "type": { + "id": { "type": "string", - "enum": ["sync"] + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "id": { + "parentID": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "syncEvent": { + "fork": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.updated.1"] - }, - "id": { + "sessionID": { "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", + "allOf": [ + { "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" } - }, - "required": ["sessionID", "info"], - "additionalProperties": false + ] + }, + "boundary": { + "$ref": "#/components/schemas/Session.ForkBoundary" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["sessionID", "boundary"], "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionDeleted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] }, - "id": { - "type": "string", - "pattern": "^evt_" + "projectID": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" }, - "syncEvent": { + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.deleted.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" }, - "aggregateID": { - "type": "string" + "updated": { + "type": "number" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false + "archived": { + "type": "number" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created", "updated"], "additionalProperties": false + }, + "title": { + "type": "string" + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "subpath": { + "type": "string" + }, + "revert": { + "$ref": "#/components/schemas/Session.Revert" } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "projectID", "cost", "tokens", "time", "location"], "additionalProperties": false }, - "SyncEventMessageUpdated": { + "SessionsResponse": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Info" + } }, - "syncEvent": { + "cursor": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["message.updated.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" + "next": { + "anyOf": [ + { + "type": "string" }, - "info": { - "$ref": "#/components/schemas/Message" + { + "type": "null" } - }, - "required": ["sessionID", "info"], - "additionalProperties": false + ] } }, - "required": ["type", "id", "seq", "aggregateID", "data"], "additionalProperties": false } }, - "required": ["type", "id", "syncEvent"], + "required": ["data", "cursor"], "additionalProperties": false }, - "SyncEventMessageRemoved": { + "InvalidCursorError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", - "enum": ["sync"] + "enum": ["InvalidCursorError"] }, - "id": { + "message": { + "type": "string" + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "InvalidRequestError1": { + "type": "object", + "properties": { + "_tag": { "type": "string", - "pattern": "^evt_" + "enum": ["InvalidRequestError"] }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["message.removed.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" + "message": { + "type": "string" + }, + "kind": { + "anyOf": [ + { + "type": "string" }, - "aggregateID": { + { + "type": "null" + } + ] + }, + "field": { + "anyOf": [ + { "type": "string" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["sessionID", "messageID"], - "additionalProperties": false + { + "type": "null" } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + ] } }, - "required": ["type", "id", "syncEvent"], + "required": ["_tag", "message"], "additionalProperties": false }, - "SyncEventMessagePartUpdated": { + "Session.Message.AgentSelected": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["message.part.updated.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "part": { - "$ref": "#/components/schemas/Part" - }, - "time": { - "type": "number" - } - }, - "required": ["sessionID", "part", "time"], - "additionalProperties": false } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["agent-switched"] + }, + "agent": { + "type": "string" + }, + "previous": { + "type": "string" } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "time", "type", "agent"], "additionalProperties": false }, - "SyncEventMessagePartRemoved": { + "Session.Message.ModelSelected": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["message.part.removed.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } - }, - "required": ["sessionID", "messageID", "partID"], - "additionalProperties": false } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["model-switched"] + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "previous": { + "$ref": "#/components/schemas/Model.Ref" } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "time", "type", "model"], "additionalProperties": false }, - "SyncEventSessionNextAgentSwitched": { + "Session.Message.LocationSwitched": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.agent.switched.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "agent"], - "additionalProperties": false + "created": { + "type": "number" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextModelSwitched": { - "type": "object", - "properties": { + }, "type": { "type": "string", - "enum": ["sync"] + "enum": ["location-switched"] }, - "id": { - "type": "string", - "pattern": "^evt_" + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "projectID": { + "type": "string" }, - "syncEvent": { + "subpath": { + "type": "string" + }, + "previous": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.model.switched.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "aggregateID": { + "projectID": { "type": "string" }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - } - }, - "required": ["timestamp", "sessionID", "messageID", "model"], - "additionalProperties": false + "subpath": { + "type": "string" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["location"], "additionalProperties": false } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "time", "type", "location"], "additionalProperties": false }, - "SyncEventSessionNextMoved": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { + "Prompt.Base64": { + "type": "string", + "allOf": [ + { + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" + } + ] + }, + "Prompt.FileSource": { + "anyOf": [ + { "type": "object", "properties": { "type": { "type": "string", - "enum": ["session.next.moved.1"] - }, - "id": { + "enum": ["inline"] + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" + "enum": ["uri"] }, - "aggregateID": { + "uri": { "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "subdirectory": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "location"], - "additionalProperties": false } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["type", "uri"], "additionalProperties": false } + ] + }, + "Prompt.Mention": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "text": { + "type": "string" + } }, - "required": ["type", "id", "syncEvent"], + "required": ["start", "end", "text"], "additionalProperties": false }, - "SyncEventSessionNextPrompted": { + "Prompt.FileAttachment": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] + "data": { + "$ref": "#/components/schemas/Prompt.Base64" + }, + "mime": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/Prompt.FileSource" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } + }, + "required": ["data", "mime", "source"], + "additionalProperties": false + }, + "Prompt.AgentAttachment": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } + }, + "required": ["name"], + "additionalProperties": false + }, + "Prompt.SkillAttachment": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" }, + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } + }, + "required": ["id", "name", "text"], + "additionalProperties": false + }, + "Session.Message.User": { + "type": "object", + "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "syncEvent": { + "metadata": { + "type": "object" + }, + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.prompted.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false + }, + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.SkillAttachment" + } + }, + "type": { + "type": "string", + "enum": ["user"] } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "time", "text", "type"], "additionalProperties": false }, - "SyncEventSessionNextPromptAdmitted": { + "Session.Message.Synthetic": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.prompt.admitted.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] - } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["synthetic"] } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "time", "text", "type"], "additionalProperties": false }, - "SyncEventSessionNextContextUpdated": { + "Session.Message.System": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.context.updated.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["system"] + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "time", "type", "text"], "additionalProperties": false }, - "SyncEventSessionNextSynthetic": { + "Session.Message.Skill": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.synthetic.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["skill"] + }, + "skill": { + "type": "string" + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "time", "type", "skill", "name", "text"], "additionalProperties": false }, - "SyncEventSessionNextShellStarted": { + "Session.Message.Shell": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.shell.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "command": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "callID", "command"], - "additionalProperties": false + "completed": { + "type": "number" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextShellEnded": { - "type": "object", - "properties": { + }, "type": { "type": "string", - "enum": ["sync"] + "enum": ["shell"] }, - "id": { + "shellID": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^sh_" + } + ] }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.shell.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { + "command": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["running", "exited", "timeout", "killed"] + }, + "exit": { + "anyOf": [ + { + "anyOf": [ + { "type": "number" }, - "sessionID": { + { "type": "string", - "pattern": "^ses" + "enum": ["NaN"] }, - "callID": { - "type": "string" + { + "type": "string", + "enum": ["Infinity"] }, - "output": { - "type": "string" + { + "type": "string", + "enum": ["-Infinity"] } - }, - "required": ["timestamp", "sessionID", "callID", "output"], - "additionalProperties": false + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "output": { + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["output", "cursor", "size", "truncated"], "additionalProperties": false } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "time", "type", "shellID", "command", "status"], + "additionalProperties": false + }, + "Session.Message.ProviderState": { + "type": "object" + }, + "Session.Message.Assistant.Text": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text"] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + } + }, + "required": ["type", "text"], "additionalProperties": false }, - "SyncEventSessionNextStepStarted": { + "Session.Message.Assistant.Reasoning": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["sync"] + "enum": ["reasoning"] }, - "id": { - "type": "string", - "pattern": "^evt_" + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.step.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "snapshot": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], - "additionalProperties": false + "completed": { + "type": "number" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false } }, - "required": ["type", "id", "syncEvent"], + "required": ["type", "text"], "additionalProperties": false }, - "SyncEventSessionNextStepEnded": { + "Session.Message.ToolState.Streaming": { "type": "object", "properties": { - "type": { + "status": { "type": "string", - "enum": ["sync"] + "enum": ["streaming"] }, - "id": { + "input": { + "type": "string" + } + }, + "required": ["status", "input"], + "additionalProperties": false + }, + "Session.Message.ToolState.Running": { + "type": "object", + "properties": { + "status": { "type": "string", - "pattern": "^evt_" + "enum": ["running"] }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.step.ended.2"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "finish": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "snapshot": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + "input": { + "type": "object" + }, + "metadata": { + "type": "object" } }, - "required": ["type", "id", "syncEvent"], + "required": ["status", "input", "metadata"], "additionalProperties": false }, - "SyncEventSessionNextStepFailed": { + "Tool.TextContent": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["sync"] + "enum": ["text"] }, - "id": { + "text": { + "type": "string" + } + }, + "required": ["type", "text"], + "additionalProperties": false + }, + "Tool.FileContent": { + "type": "object", + "properties": { + "type": { "type": "string", - "pattern": "^evt_" + "enum": ["file"] }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.step.failed.2"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { + "uri": { + "type": "string" + }, + "mime": { + "type": "string" + }, + "name": { + "anyOf": [ + { "type": "string" }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "error"], - "additionalProperties": false + { + "type": "null" } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + ] } }, - "required": ["type", "id", "syncEvent"], + "required": ["type", "uri", "mime"], "additionalProperties": false }, - "SyncEventSessionNextTextStarted": { + "Tool.Content": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tool.TextContent" + }, + { + "$ref": "#/components/schemas/Tool.FileContent" + } + ] + }, + "Session.Message.ToolState.Completed": { "type": "object", "properties": { - "type": { + "status": { "type": "string", - "enum": ["sync"] + "enum": ["completed"] }, - "id": { - "type": "string", - "pattern": "^evt_" + "input": { + "type": "object" }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.text.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], - "additionalProperties": false + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Tool.Content" } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Tool.Content" + } + }, + "metadata": { + "type": "object" + } + }, + "required": ["status", "input", "content"], + "additionalProperties": false + }, + "Session.StructuredError": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer", + "allOf": [ + { + "minimum": 100, + "maximum": 599 + } + ] } }, - "required": ["type", "id", "syncEvent"], + "required": ["type", "message"], "additionalProperties": false }, - "SyncEventSessionNextTextEnded": { + "Session.Message.ToolState.Error": { "type": "object", "properties": { - "type": { + "status": { "type": "string", - "enum": ["sync"] + "enum": ["error"] }, - "id": { - "type": "string", - "pattern": "^evt_" + "input": { + "type": "object" }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.text.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], - "additionalProperties": false + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Tool.Content" } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Tool.Content" + } + }, + "metadata": { + "type": "object" } }, - "required": ["type", "id", "syncEvent"], + "required": ["status", "input", "error"], "additionalProperties": false }, - "SyncEventSessionNextReasoningStarted": { + "Session.Message.Assistant.Tool": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["sync"] + "enum": ["tool"] }, "id": { - "type": "string", - "pattern": "^evt_" + "type": "string" }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.reasoning.started.1"] + "name": { + "type": "string" + }, + "executed": { + "type": "boolean" + }, + "providerState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "providerResultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "state": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" }, - "id": { - "type": "string", - "pattern": "^evt_" + { + "$ref": "#/components/schemas/Session.Message.ToolState.Running" }, - "seq": { + { + "$ref": "#/components/schemas/Session.Message.ToolState.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Error" + } + ] + }, + "time": { + "type": "object", + "properties": { + "created": { "type": "number" }, - "aggregateID": { - "type": "string" + "ran": { + "type": "number" }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], - "additionalProperties": false + "completed": { + "type": "number" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false } }, - "required": ["type", "id", "syncEvent"], + "required": ["type", "id", "name", "state", "time"], "additionalProperties": false }, - "SyncEventSessionNextReasoningEnded": { + "Session.Message.Assistant.Retry": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] + "attempt": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] }, - "id": { - "type": "string", - "pattern": "^evt_" + "at": { + "type": "number" }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.reasoning.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" - }, - "text": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + "error": { + "$ref": "#/components/schemas/Session.StructuredError" } }, - "required": ["type", "id", "syncEvent"], + "required": ["attempt", "at", "error"], "additionalProperties": false }, - "SyncEventSessionNextToolInputStarted": { + "Session.Message.Assistant": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.input.started.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], - "additionalProperties": false + "completed": { + "type": "number" } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false - } - }, - "required": ["type", "id", "syncEvent"], - "additionalProperties": false - }, - "SyncEventSessionNextToolInputEnded": { - "type": "object", - "properties": { + }, "type": { "type": "string", - "enum": ["sync"] + "enum": ["assistant"] }, - "id": { - "type": "string", - "pattern": "^evt_" + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "content": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Assistant.Text" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Reasoning" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Tool" + } + ] + } }, - "syncEvent": { + "snapshot": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.input.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { + "start": { "type": "string" }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], - "additionalProperties": false + "end": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": ["type", "id", "seq", "aggregateID", "data"], "additionalProperties": false + }, + "finish": { + "type": "string", + "enum": ["stop", "length", "tool-calls", "content-filter", "error", "unknown"] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "retry": { + "$ref": "#/components/schemas/Session.Message.Assistant.Retry" } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "time", "type", "agent", "model", "content"], "additionalProperties": false }, - "SyncEventSessionNextToolCalled": { + "Session.Message.Compaction.Running": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["sync"] + "enum": ["compaction"] }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.called.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "input": { - "type": "object" - }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], - "additionalProperties": false } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false + }, + "status": { + "type": "string", + "enum": ["running"] + }, + "reason": { + "type": "string", + "enum": ["auto", "manual"] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" } }, - "required": ["type", "id", "syncEvent"], + "required": ["type", "id", "time", "status", "reason", "summary", "recent"], "additionalProperties": false }, - "SyncEventSessionNextToolProgress": { + "Session.Message.Compaction.Completed": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["sync"] + "enum": ["compaction"] }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" }, - "syncEvent": { + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.progress.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], - "additionalProperties": false } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["created"], "additionalProperties": false + }, + "status": { + "type": "string", + "enum": ["completed"] + }, + "reason": { + "type": "string", + "enum": ["auto", "manual"] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" } }, - "required": ["type", "id", "syncEvent"], + "required": ["type", "id", "time", "status", "reason", "summary", "recent"], "additionalProperties": false }, - "SyncEventSessionNextToolSuccess": { + "Session.Message.Compaction.Failed": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["sync"] + "enum": ["compaction"] }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "syncEvent": { + "metadata": { + "type": "object" + }, + "time": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.success.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { + "created": { "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "structured", - "content", - "provider" - ], - "additionalProperties": false } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + }, + "required": ["created"], + "additionalProperties": false + }, + "status": { + "type": "string", + "enum": ["failed"] + }, + "reason": { + "type": "string", + "enum": ["auto", "manual"] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": ["type", "id", "time", "status", "reason", "error"], + "additionalProperties": false + }, + "Session.Message.Compaction": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Compaction.Running" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction.Failed" + } + ] + }, + "Session.Message.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.AgentSelected" + }, + { + "$ref": "#/components/schemas/Session.Message.ModelSelected" + }, + { + "$ref": "#/components/schemas/Session.Message.LocationSwitched" + }, + { + "$ref": "#/components/schemas/Session.Message.User" + }, + { + "$ref": "#/components/schemas/Session.Message.Synthetic" + }, + { + "$ref": "#/components/schemas/Session.Message.System" + }, + { + "$ref": "#/components/schemas/Session.Message.Skill" + }, + { + "$ref": "#/components/schemas/Session.Message.Shell" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction" + } + ] + }, + "ConflictError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ConflictError"] + }, + "message": { + "type": "string" + }, + "resource": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, - "required": ["type", "id", "syncEvent"], + "required": ["_tag", "message"], "additionalProperties": false }, - "SyncEventSessionNextToolFailed": { + "SessionTransfer.Data": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] + "info": { + "$ref": "#/components/schemas/Session.Info" }, - "id": { + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message.Info" + } + } + }, + "required": ["info", "messages"], + "additionalProperties": false + }, + "SessionNotFoundError": { + "type": "object", + "properties": { + "_tag": { "type": "string", - "pattern": "^evt_" + "enum": ["SessionNotFoundError"] }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.tool.failed.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + "sessionID": { + "type": "string" + }, + "message": { + "type": "string" } }, - "required": ["type", "id", "syncEvent"], + "required": ["_tag", "sessionID", "message"], "additionalProperties": false }, - "SyncEventSessionNextRetried": { + "UnknownError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", - "enum": ["sync"] + "enum": ["UnknownError"] }, - "id": { - "type": "string", - "pattern": "^evt_" + "message": { + "type": "string" }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.retried.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { + "ref": { + "anyOf": [ + { "type": "string" }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "attempt": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/SessionNextRetry_error" - } - }, - "required": ["timestamp", "sessionID", "attempt", "error"], - "additionalProperties": false + { + "type": "null" } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + ] } }, - "required": ["type", "id", "syncEvent"], + "required": ["_tag", "message"], "additionalProperties": false }, - "SyncEventSessionNextCompactionStarted": { + "SessionActive": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["sync"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "syncEvent": { + "enum": ["running"] + } + }, + "required": ["type"], + "additionalProperties": false + }, + "Session.ForkRequestBoundary": { + "anyOf": [ + { "type": "object", "properties": { "type": { "type": "string", - "enum": ["session.next.compaction.started.1"] + "enum": ["before"] }, - "id": { + "messageID": { "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", + "allOf": [ + { "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] } - }, - "required": ["timestamp", "sessionID", "messageID", "reason"], - "additionalProperties": false + ] + } + }, + "required": ["type", "messageID"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["through"] } }, - "required": ["type", "id", "seq", "aggregateID", "data"], + "required": ["type"], "additionalProperties": false } + ] + }, + "MessageNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["MessageNotFoundError"] + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "message": { + "type": "string" + } }, - "required": ["type", "id", "syncEvent"], + "required": ["_tag", "sessionID", "messageID", "message"], "additionalProperties": false }, - "SyncEventSessionNextCompactionEnded": { + "Session.Inbox.Delivery": { + "type": "string", + "enum": ["steer", "queue"] + }, + "PromptInput.FileAttachment": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] + "uri": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" }, + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } + }, + "required": ["uri"], + "additionalProperties": false + }, + "PromptInput.SkillAttachment": { + "type": "object", + "properties": { "id": { - "type": "string", - "pattern": "^evt_" + "type": "string" }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.compaction.ended.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - }, - "text": { - "type": "string" - }, - "recent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "Session.Inbox.UserPayload": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.SkillAttachment" + } + }, + "metadata": { + "type": "object" } }, - "required": ["type", "id", "syncEvent"], + "required": ["text"], "additionalProperties": false }, - "SyncEventSessionNextRevertStaged": { + "Session.Inbox.User": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["sync"] - }, "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.revert.staged.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "revert": { - "$ref": "#/components/schemas/RevertState" - } - }, - "required": ["timestamp", "sessionID", "revert"], - "additionalProperties": false + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + ] + }, + "timeCreated": { + "type": "number" + }, + "type": { + "type": "string", + "enum": ["user"] + }, + "payload": { + "$ref": "#/components/schemas/Session.Inbox.UserPayload" + }, + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" } }, - "required": ["type", "id", "syncEvent"], + "required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"], "additionalProperties": false }, - "SyncEventSessionNextRevertCleared": { + "CommandNotFoundError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", - "enum": ["sync"] + "enum": ["CommandNotFoundError"] }, - "id": { - "type": "string", - "pattern": "^evt_" + "command": { + "type": "string" }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.revert.cleared.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["timestamp", "sessionID"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + "message": { + "type": "string" } }, - "required": ["type", "id", "syncEvent"], + "required": ["_tag", "command", "message"], "additionalProperties": false }, - "SyncEventSessionNextRevertCommitted": { + "CommandEvaluationError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", - "enum": ["sync"] + "enum": ["CommandEvaluationError"] }, - "id": { - "type": "string", - "pattern": "^evt_" + "command": { + "type": "string" }, - "syncEvent": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["session.next.revert.committed.1"] - }, - "id": { - "type": "string", - "pattern": "^evt_" - }, - "seq": { - "type": "number" - }, - "aggregateID": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - } - }, - "required": ["timestamp", "sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["type", "id", "seq", "aggregateID", "data"], - "additionalProperties": false + "message": { + "type": "string" } }, - "required": ["type", "id", "syncEvent"], + "required": ["_tag", "command", "message"], "additionalProperties": false }, - "ConfigV2ReferenceGit": { + "SkillNotFoundError": { "type": "object", "properties": { - "repository": { - "type": "string" + "_tag": { + "type": "string", + "enum": ["SkillNotFoundError"] }, - "branch": { + "skill": { "type": "string" }, - "description": { + "message": { "type": "string" - }, - "hidden": { - "type": "boolean" } }, - "required": ["repository"], + "required": ["_tag", "skill", "message"], "additionalProperties": false }, - "ConfigV2ReferenceLocal": { + "Session.Inbox.SyntheticPayload": { "type": "object", "properties": { - "path": { + "text": { "type": "string" }, "description": { "type": "string" }, - "hidden": { - "type": "boolean" + "metadata": { + "type": "object" } }, - "required": ["path"], + "required": ["text"], "additionalProperties": false }, - "PolicyEffect": { - "type": "string", - "enum": ["allow", "deny"] - }, - "ConfigV2ExperimentalPolicy": { + "Session.Inbox.Synthetic": { "type": "object", "properties": { - "action": { + "id": { "type": "string", - "enum": ["provider.use"] + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "effect": { - "$ref": "#/components/schemas/PolicyEffect" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "resource": { - "type": "string" + "timeCreated": { + "type": "number" + }, + "type": { + "type": "string", + "enum": ["synthetic"] + }, + "payload": { + "$ref": "#/components/schemas/Session.Inbox.SyntheticPayload" + }, + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" } }, - "required": ["action", "effect", "resource"], + "required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"], "additionalProperties": false }, - "ProjectDirectories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "strategy": { - "type": "string" - } + "Session.Inbox.CompactionPayload": { + "anyOf": [ + { + "type": "object" }, - "required": ["directory"], - "additionalProperties": false - } + { + "type": "array" + } + ] }, - "PtyTicketConnectToken": { + "Session.Inbox.Compaction": { "type": "object", "properties": { - "ticket": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "timeCreated": { + "type": "number" + }, + "type": { + "type": "string", + "enum": ["compaction"] + }, + "payload": { + "$ref": "#/components/schemas/Session.Inbox.CompactionPayload" + }, + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + } + }, + "required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"], + "additionalProperties": false + }, + "ServiceUnavailableError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ServiceUnavailableError"] + }, + "message": { "type": "string" }, - "expires_in": { - "type": "integer", - "exclusiveMinimum": 0 + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, - "required": ["ticket", "expires_in"], + "required": ["_tag", "message"], "additionalProperties": false }, - "WorkspaceEventConnectionStatus": { + "SessionBusyError": { "type": "object", "properties": { - "workspaceID": { + "_tag": { "type": "string", - "pattern": "^wrk" + "enum": ["SessionBusyError"] + }, + "sessionID": { + "type": "string" }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] + "message": { + "type": "string" } }, - "required": ["workspaceID", "status"], + "required": ["_tag", "sessionID", "message"], "additionalProperties": false }, - "LocationInfo": { + "Session.Inbox.MovePayload": { "type": "object", "properties": { - "directory": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "workspaceID": { - "type": "string", - "pattern": "^wrk" + "projectID": { + "type": "string" }, - "project": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "directory": { - "type": "string" - } - }, - "required": ["id", "directory"], - "additionalProperties": false + "subpath": { + "type": "string" } }, - "required": ["directory", "project"], + "required": ["location", "projectID"], "additionalProperties": false }, - "ProviderRequest": { + "Session.Inbox.Move": { "type": "object", "properties": { - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "body": { - "type": "object" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "timeCreated": { + "type": "number" + }, + "type": { + "type": "string", + "enum": ["move"] + }, + "payload": { + "$ref": "#/components/schemas/Session.Inbox.MovePayload" + }, + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" } }, - "required": ["headers", "body"], + "required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"], "additionalProperties": false }, - "AgentColor": { + "Session.Inbox.Info": { "anyOf": [ { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$" + "$ref": "#/components/schemas/Session.Inbox.User" }, { - "type": "string", - "enum": ["primary", "secondary", "accent", "success", "warning", "error", "info"] + "$ref": "#/components/schemas/Session.Inbox.Synthetic" + }, + { + "$ref": "#/components/schemas/Session.Inbox.Compaction" + }, + { + "$ref": "#/components/schemas/Session.Inbox.Move" } ] }, - "PermissionV2Effect": { + "InstructionEntry.Key": { "type": "string", - "enum": ["allow", "deny", "ask"] + "allOf": [ + { + "pattern": "^[a-z0-9][a-z0-9._-]*$", + "description": "Instruction entry key (lowercase alphanumerics plus . _ -)" + } + ] }, - "PermissionV2Rule": { + "InstructionEntry.Info": { "type": "object", "properties": { - "action": { - "type": "string" - }, - "resource": { - "type": "string" + "key": { + "$ref": "#/components/schemas/InstructionEntry.Key" }, - "effect": { - "$ref": "#/components/schemas/PermissionV2Effect" - } + "value": {} }, - "required": ["action", "resource", "effect"], + "required": ["key", "value"], "additionalProperties": false }, - "PermissionV2Ruleset": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2Rule" - } - }, - "AgentV2Info": { + "InstructionEntryValueTooLargeError": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "request": { - "$ref": "#/components/schemas/ProviderRequest" - }, - "system": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mode": { + "_tag": { "type": "string", - "enum": ["subagent", "primary", "all"] - }, - "hidden": { - "type": "boolean" + "enum": ["InstructionEntryValueTooLargeError"] }, - "color": { - "$ref": "#/components/schemas/AgentColor" + "actualBytes": { + "type": "integer" }, - "steps": { - "type": "integer", - "exclusiveMinimum": 0 + "maxBytes": { + "type": "integer" }, - "permissions": { - "$ref": "#/components/schemas/PermissionV2Ruleset" + "message": { + "type": "string" } }, - "required": ["id", "request", "mode", "hidden", "permissions"], + "required": ["_tag", "actualBytes", "maxBytes", "message"], "additionalProperties": false }, - "SessionV2Info": { + "SessionGenerateResponse": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^ses" - }, - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "projectID": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "time": { + "data": { "type": "object", "properties": { - "created": { - "type": "number" - }, - "updated": { - "type": "number" - }, - "archived": { - "type": "number" + "text": { + "type": "string" } }, - "required": ["created", "updated"], + "required": ["text"], "additionalProperties": false - }, - "title": { - "type": "string" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "subpath": { - "type": "string" - }, - "revert": { - "$ref": "#/components/schemas/RevertState" - } - }, - "required": ["id", "projectID", "cost", "tokens", "time", "title", "location"], - "additionalProperties": false - }, - "PromptInputFileAttachment": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/PromptSource" } }, - "required": ["uri"], + "required": ["data"], "additionalProperties": false }, - "SessionInputAdmitted": { + "session.created": { "type": "object", "properties": { - "admittedSeq": { - "type": "integer", - "minimum": 0 - }, "id": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "sessionID": { - "type": "string", - "pattern": "^ses" + "created": { + "type": "number" }, - "prompt": { - "$ref": "#/components/schemas/Prompt" + "metadata": { + "type": "object" }, - "delivery": { + "type": { "type": "string", - "enum": ["steer", "queue"] + "enum": ["session.created"] }, - "timeCreated": { - "type": "number" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [1] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, - "promotedSeq": { - "type": "integer", - "minimum": 0 + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "projectID": { + "type": "string" + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "subpath": { + "type": "string" + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "slug": { + "type": "string" + }, + "title": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "version": { + "type": "string" + } + }, + "required": ["sessionID", "projectID", "location", "slug", "version"], + "additionalProperties": false } }, - "required": ["admittedSeq", "id", "sessionID", "prompt", "delivery", "timeCreated"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageAgentSwitched": { + "session.agent.selected": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, - "time": { + "type": { + "type": "string", + "enum": ["session.agent.selected"] + }, + "durable": { "type": "object", "properties": { - "created": { - "type": "number" + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [1] } }, - "required": ["created"], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, - "type": { - "type": "string", - "enum": ["agent-switched"] + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "agent": { - "type": "string" + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "agent": { + "type": "string" + }, + "previous": { + "type": "string" + } + }, + "required": ["sessionID", "agent"], + "additionalProperties": false } }, - "required": ["id", "time", "type", "agent"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageModelSwitched": { + "session.model.selected": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, - "time": { + "type": { + "type": "string", + "enum": ["session.model.selected"] + }, + "durable": { "type": "object", "properties": { - "created": { - "type": "number" + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [1] } }, - "required": ["created"], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, - "type": { - "type": "string", - "enum": ["model-switched"] + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "model": { - "$ref": "#/components/schemas/ModelRef" + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "previous": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": ["sessionID", "model"], + "additionalProperties": false } }, - "required": ["id", "time", "type", "model"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageUser": { + "session.moved": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, - "time": { + "type": { + "type": "string", + "enum": ["session.moved"] + }, + "durable": { "type": "object", "properties": { - "created": { - "type": "number" + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [1] } }, - "required": ["created"], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, - "text": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptFileAttachment" - } - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PromptAgentAttachment" - } + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "type": { - "type": "string", - "enum": ["user"] + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "projectID": { + "type": "string" + }, + "subpath": { + "type": "string" + } + }, + "required": ["sessionID", "location", "projectID"], + "additionalProperties": false } }, - "required": ["id", "time", "text", "type"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageSynthetic": { + "session.renamed": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, - "time": { + "type": { + "type": "string", + "enum": ["session.renamed"] + }, + "durable": { "type": "object", "properties": { - "created": { - "type": "number" + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [1] } }, - "required": ["created"], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "text": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "type": { - "type": "string", - "enum": ["synthetic"] + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "title": { + "type": "string" + } + }, + "required": ["sessionID", "title"], + "additionalProperties": false } }, - "required": ["id", "time", "sessionID", "text", "type"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageSystem": { + "session.deleted": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, - "time": { + "type": { + "type": "string", + "enum": ["session.deleted"] + }, + "durable": { "type": "object", "properties": { - "created": { - "type": "number" + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [2] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] } }, - "required": ["created"], + "required": ["sessionID"], "additionalProperties": false - }, - "type": { - "type": "string", - "enum": ["system"] - }, - "text": { - "type": "string" } }, - "required": ["id", "time", "type", "text"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageShell": { + "session.forked": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, - "time": { + "type": { + "type": "string", + "enum": ["session.forked"] + }, + "durable": { "type": "object", "properties": { - "created": { - "type": "number" + "aggregateID": { + "type": "string" }, - "completed": { - "type": "number" + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [2] } }, - "required": ["created"], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, - "type": { - "type": "string", - "enum": ["shell"] - }, - "callID": { - "type": "string" - }, - "command": { - "type": "string" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "output": { - "type": "string" + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "boundary": { + "$ref": "#/components/schemas/Session.ForkBoundary" + }, + "instructions": { + "type": "object", + "patternProperties": { + "^[a-z0-9][a-z0-9._-]*\\/[a-z0-9][a-z0-9._/-]*$": { + "type": "string", + "allOf": [ + { + "pattern": "^[a-f0-9]{64}$" + } + ] + } + } + } + }, + "required": ["sessionID", "parentID", "boundary"], + "additionalProperties": false } }, - "required": ["id", "time", "type", "callID", "command", "output"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageAssistantText": { + "session.inbox.delivered": { "type": "object", "properties": { - "type": { + "id": { "type": "string", - "enum": ["text"] + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "id": { - "type": "string" + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, - "text": { - "type": "string" - } - }, - "required": ["type", "id", "text"], - "additionalProperties": false - }, - "SessionMessageAssistantReasoning": { - "type": "object", - "properties": { "type": { "type": "string", - "enum": ["reasoning"] - }, - "id": { - "type": "string" + "enum": ["session.inbox.delivered"] }, - "text": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [1] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "time": { + "data": { "type": "object", "properties": { - "created": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "completed": { - "type": "number" + "inboxID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, - "required": ["created"], + "required": ["sessionID", "inboxID"], "additionalProperties": false } }, - "required": ["type", "id", "text"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageToolStatePending": { + "Session.Inbox.UserPayload1": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": ["pending"] - }, - "input": { + "text": { "type": "string" - } - }, - "required": ["status", "input"], - "additionalProperties": false - }, - "SessionMessageToolStateRunning": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["running"] - }, - "input": { - "type": "object" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - } - }, - "required": ["status", "input", "structured", "content"], - "additionalProperties": false - }, - "SessionMessageToolStateCompleted": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["completed"] - }, - "input": { - "type": "object" }, - "attachments": { + "files": { "type": "array", "items": { - "$ref": "#/components/schemas/PromptFileAttachment" + "$ref": "#/components/schemas/Prompt.FileAttachment" } }, - "content": { + "agents": { "type": "array", "items": { - "$ref": "#/components/schemas/LLMToolContent" + "$ref": "#/components/schemas/Prompt.AgentAttachment" } }, - "outputPaths": { + "skills": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/Prompt.SkillAttachment" } }, - "structured": { + "metadata": { "type": "object" + } + }, + "required": ["text"], + "additionalProperties": false + }, + "Session.Inbox.SyntheticPayload1": { + "type": "object", + "properties": { + "text": { + "type": "string" }, - "result": {} + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } }, - "required": ["status", "input", "content", "structured"], + "required": ["text"], "additionalProperties": false }, - "SessionMessageToolStateError": { + "Session.Inbox.Item": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["user"] + }, + "payload": { + "$ref": "#/components/schemas/Session.Inbox.UserPayload1" + }, + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + } + }, + "required": ["type", "payload", "delivery"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["synthetic"] + }, + "payload": { + "$ref": "#/components/schemas/Session.Inbox.SyntheticPayload1" + }, + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + } + }, + "required": ["type", "payload", "delivery"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["compaction"] + }, + "payload": { + "$ref": "#/components/schemas/Session.Inbox.CompactionPayload" + }, + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + } + }, + "required": ["type", "payload", "delivery"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["move"] + }, + "payload": { + "$ref": "#/components/schemas/Session.Inbox.MovePayload" + }, + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + } + }, + "required": ["type", "payload", "delivery"], + "additionalProperties": false + } + ] + }, + "session.inbox.enqueued": { "type": "object", "properties": { - "status": { + "id": { "type": "string", - "enum": ["error"] - }, - "input": { - "type": "object" + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } + "created": { + "type": "number" }, - "structured": { + "metadata": { "type": "object" }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - }, - "result": {} - }, - "required": ["status", "input", "content", "structured", "error"], - "additionalProperties": false - }, - "SessionMessageAssistantTool": { - "type": "object", - "properties": { "type": { "type": "string", - "enum": ["tool"] - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" + "enum": ["session.inbox.enqueued"] }, - "provider": { + "durable": { "type": "object", "properties": { - "executed": { - "type": "boolean" + "aggregateID": { + "type": "string" }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "resultMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + "version": { + "type": "number", + "enum": [1] } }, - "required": ["executed"], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, - "state": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionMessageToolStatePending" - }, - { - "$ref": "#/components/schemas/SessionMessageToolStateRunning" - }, - { - "$ref": "#/components/schemas/SessionMessageToolStateCompleted" - }, - { - "$ref": "#/components/schemas/SessionMessageToolStateError" - } - ] + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "time": { + "data": { "type": "object", "properties": { - "created": { - "type": "number" - }, - "ran": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "completed": { - "type": "number" + "inboxID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "pruned": { - "type": "number" + "item": { + "$ref": "#/components/schemas/Session.Inbox.Item" } }, - "required": ["created"], + "required": ["sessionID", "inboxID", "item"], "additionalProperties": false } }, - "required": ["type", "id", "name", "state", "time"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageAssistant": { + "session.inbox.cancelled": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": ["created"], - "additionalProperties": false - }, "type": { "type": "string", - "enum": ["assistant"] - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" - }, - "content": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionMessageAssistantText" - }, - { - "$ref": "#/components/schemas/SessionMessageAssistantReasoning" - }, - { - "$ref": "#/components/schemas/SessionMessageAssistantTool" - } - ] - } + "enum": ["session.inbox.cancelled"] }, - "snapshot": { + "durable": { "type": "object", "properties": { - "start": { + "aggregateID": { "type": "string" }, - "end": { - "type": "string" + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "files": { - "type": "array", - "items": { - "type": "string" - } + "version": { + "type": "number", + "enum": [1] } }, + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, - "finish": { - "type": "string" - }, - "cost": { - "type": "number" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "tokens": { + "data": { "type": "object", "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" + "inboxID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" } - }, - "required": ["read", "write"], - "additionalProperties": false + ] } }, - "required": ["input", "output", "reasoning", "cache"], + "required": ["sessionID", "inboxID"], "additionalProperties": false - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" } }, - "required": ["id", "time", "type", "agent", "model", "content"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessageCompaction": { + "session.inbox.delivery.changed": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["compaction"] - }, - "reason": { + "id": { "type": "string", - "enum": ["auto", "manual"] + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "summary": { - "type": "string" + "created": { + "type": "number" }, - "recent": { - "type": "string" + "metadata": { + "type": "object" }, - "id": { + "type": { "type": "string", - "pattern": "^msg_" + "enum": ["session.inbox.delivery.changed"] }, - "metadata": { - "type": "object" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [1] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, - "time": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { "type": "object", "properties": { - "created": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "inboxID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" } }, - "required": ["created"], + "required": ["sessionID", "inboxID", "delivery"], "additionalProperties": false } }, - "required": ["type", "reason", "summary", "recent", "id", "time"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionMessage": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionMessageAgentSwitched" - }, - { - "$ref": "#/components/schemas/SessionMessageModelSwitched" - }, - { - "$ref": "#/components/schemas/SessionMessageUser" - }, - { - "$ref": "#/components/schemas/SessionMessageSynthetic" - }, - { - "$ref": "#/components/schemas/SessionMessageSystem" - }, - { - "$ref": "#/components/schemas/SessionMessageShell" - }, - { - "$ref": "#/components/schemas/SessionMessageAssistant" - }, - { - "$ref": "#/components/schemas/SessionMessageCompaction" - } - ] - }, - "SessionNextAgentSwitched": { + "session.execution.started": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.agent.switched"] + "enum": ["session.execution.started"] }, "durable": { "type": "object", @@ -27995,56 +15381,63 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" + "allOf": [ + { + "pattern": "^ses" + } + ] } }, - "required": ["timestamp", "sessionID", "messageID", "agent"], + "required": ["sessionID"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextModelSwitched": { + "session.execution.succeeded": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.model.switched"] + "enum": ["session.execution.succeeded"] }, "durable": { "type": "object", @@ -28053,56 +15446,63 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" + "allOf": [ + { + "pattern": "^ses" + } + ] } }, - "required": ["timestamp", "sessionID", "messageID", "model"], + "required": ["sessionID"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextMoved": { + "session.execution.failed": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.moved"] + "enum": ["session.execution.failed"] }, "durable": { "type": "object", @@ -28111,55 +15511,66 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "subdirectory": { - "type": "string" + "error": { + "$ref": "#/components/schemas/Session.StructuredError" } }, - "required": ["timestamp", "sessionID", "location"], + "required": ["sessionID", "error"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextPrompted": { + "session.execution.interrupted": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.prompted"] + "enum": ["session.execution.interrupted"] }, "durable": { "type": "object", @@ -28168,60 +15579,67 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "delivery": { + "reason": { "type": "string", - "enum": ["steer", "queue"] + "enum": ["user", "shutdown", "superseded"] } }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], + "required": ["sessionID", "reason"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextPromptAdmitted": { + "session.instructions.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.prompt.admitted"] + "enum": ["session.instructions.updated"] }, "durable": { "type": "object", @@ -28230,60 +15648,85 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [2] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "prompt": { - "$ref": "#/components/schemas/Prompt" + "delta": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[a-f0-9]{64}$" + } + ] + }, + { + "type": "string", + "enum": ["removed"] + } + ] + } }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] + "text": { + "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], + "required": ["sessionID", "delta"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextContextUpdated": { + "session.synthetic": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.context.updated"] + "enum": ["session.synthetic"] }, "durable": { "type": "object", @@ -28292,56 +15735,72 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "text": { "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" } }, - "required": ["timestamp", "sessionID", "messageID", "text"], + "required": ["sessionID", "text"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextSynthetic": { + "session.skill.activated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.synthetic"] + "enum": ["session.skill.activated"] }, "durable": { "type": "object", @@ -28350,56 +15809,130 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["sessionID", "id", "name", "text"], + "additionalProperties": false + } + }, + "required": ["id", "created", "type", "durable", "data"], + "additionalProperties": false + }, + "Shell.Info": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "status": { + "type": "string", + "enum": ["running", "exited", "timeout", "killed"] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + ] }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "exit": { + "type": "number" }, - "data": { + "metadata": { + "type": "object" + }, + "time": { "type": "object", "properties": { - "timestamp": { + "started": { "type": "number" }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" + "completed": { + "type": "number" } }, - "required": ["timestamp", "sessionID", "messageID", "text"], + "required": ["started"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "status", "command", "cwd", "shell", "file", "metadata", "time"], "additionalProperties": false }, - "SessionNextShellStarted": { + "session.shell.started": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.shell.started"] + "enum": ["session.shell.started"] }, "durable": { "type": "object", @@ -28408,59 +15941,66 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "command": { - "type": "string" + "shell": { + "$ref": "#/components/schemas/Shell.Info" } }, - "required": ["timestamp", "sessionID", "messageID", "callID", "command"], + "required": ["sessionID", "shell"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextShellEnded": { + "session.shell.ended": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.shell.ended"] + "enum": ["session.shell.ended"] }, "durable": { "type": "object", @@ -28469,55 +16009,95 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "callID": { - "type": "string" + "shell": { + "$ref": "#/components/schemas/Shell.Info" }, "output": { - "type": "string" + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": ["output", "cursor", "size", "truncated"], + "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "output"], + "required": ["sessionID", "shell", "output"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextStepStarted": { + "session.step.started": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.step.started"] + "enum": ["session.step.started"] }, "durable": { "type": "object", @@ -28526,62 +16106,80 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, "agent": { "type": "string" }, "model": { - "$ref": "#/components/schemas/ModelRef" + "$ref": "#/components/schemas/Model.Ref" }, "snapshot": { "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], + "required": ["sessionID", "assistantMessageID", "agent", "model"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextStepEnded": { + "session.step.ended": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.step.ended"] + "enum": ["session.step.ended"] }, "durable": { "type": "object", @@ -28590,66 +16188,52 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, "finish": { - "type": "string" + "type": "string", + "enum": ["stop", "length", "tool-calls", "content-filter", "error", "unknown"] }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false + "$ref": "#/components/schemas/TokenUsage.Info" }, "snapshot": { "type": "string" @@ -28661,26 +16245,33 @@ } } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], + "required": ["sessionID", "assistantMessageID", "finish", "cost", "tokens"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextStepFailed": { + "session.step.failed": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.step.failed"] + "enum": ["session.step.failed"] }, "durable": { "type": "object", @@ -28689,114 +16280,89 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "error"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextTextStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.text.started"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "sessionID": { - "type": "string", - "pattern": "^ses" + "error": { + "$ref": "#/components/schemas/Session.StructuredError" }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" }, - "textID": { + "snapshot": { "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], + "required": ["sessionID", "assistantMessageID", "error"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextTextEnded": { + "session.text.started": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.text.ended"] + "enum": ["session.text.started"] }, "durable": { "type": "object", @@ -28805,59 +16371,82 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "text": { - "type": "string" + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], + "required": ["sessionID", "assistantMessageID", "ordinal"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextToolInputStarted": { + "Session.Message.ProviderState4": { + "type": "object" + }, + "session.text.ended": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.input.started"] + "enum": ["session.text.ended"] }, "durable": { "type": "object", @@ -28866,59 +16455,88 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "callID": { - "type": "string" + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "name": { + "text": { "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState4" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], + "required": ["sessionID", "assistantMessageID", "ordinal", "text"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextToolInputEnded": { + "Session.Message.ProviderState5": { + "type": "object" + }, + "session.reasoning.started": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.input.ended"] + "enum": ["session.reasoning.started"] }, "durable": { "type": "object", @@ -28927,59 +16545,85 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "callID": { - "type": "string" + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "text": { - "type": "string" + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState5" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], + "required": ["sessionID", "assistantMessageID", "ordinal"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextToolCalled": { + "Session.Message.ProviderState6": { + "type": "object" + }, + "session.reasoning.ended": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.called"] + "enum": ["session.reasoning.ended"] }, "durable": { "type": "object", @@ -28988,75 +16632,85 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "callID": { - "type": "string" + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "tool": { + "text": { "type": "string" }, - "input": { - "type": "object" - }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState6" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], + "required": ["sessionID", "assistantMessageID", "ordinal", "text"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextToolProgress": { + "session.tool.input.started": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.progress"] + "enum": ["session.tool.input.started"] }, "durable": { "type": "object", @@ -29065,65 +16719,77 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "callID": { + "id": { "type": "string" }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } + "name": { + "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], + "required": ["sessionID", "assistantMessageID", "id", "name"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextToolSuccess": { + "session.tool.input.ended": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.success"] + "enum": ["session.tool.input.ended"] }, "durable": { "type": "object", @@ -29132,85 +16798,80 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "callID": { + "id": { "type": "string" }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false + "text": { + "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], + "required": ["sessionID", "assistantMessageID", "id", "text"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextToolFailed": { + "Session.Message.ProviderState7": { + "type": "object" + }, + "session.tool.called": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.failed"] + "enum": ["session.tool.called"] }, "durable": { "type": "object", @@ -29219,73 +16880,123 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "callID": { + "id": { "type": "string" }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" + "input": { + "type": "object" }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false + "executed": { + "type": "boolean" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState7" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], + "required": ["sessionID", "assistantMessageID", "id", "input", "executed"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], + "additionalProperties": false + }, + "Tool.FileContent1": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["file"] + }, + "uri": { + "type": "string" + }, + "mime": { + "type": "string" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["type", "uri", "mime"], "additionalProperties": false }, - "SessionNextReasoningStarted": { + "Tool.Content1": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tool.TextContent" + }, + { + "$ref": "#/components/schemas/Tool.FileContent1" + } + ] + }, + "Session.Message.ProviderState8": { + "type": "object" + }, + "session.tool.success": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.reasoning.started"] + "enum": ["session.tool.success"] }, "durable": { "type": "object", @@ -29294,59 +17005,98 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [2] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "reasoningID": { + "id": { "type": "string" }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Tool.Content1" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Tool.Content1" + } + }, + "metadata": { + "type": "object" + }, + "executed": { + "type": "boolean" + }, + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState8" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], + "required": ["sessionID", "assistantMessageID", "id", "content", "executed"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextReasoningEnded": { + "Session.Message.ProviderState9": { + "type": "object" + }, + "session.tool.failed": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.reasoning.ended"] + "enum": ["session.tool.failed"] }, "durable": { "type": "object", @@ -29355,62 +17105,98 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [2] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "assistantMessageID": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "reasoningID": { + "id": { "type": "string" }, - "text": { - "type": "string" + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "content": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Tool.Content1" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Tool.Content1" + } + }, + "metadata": { + "type": "object" + }, + "executed": { + "type": "boolean" }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState9" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], + "required": ["sessionID", "assistantMessageID", "id", "error", "executed"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextRetried": { + "session.retry.scheduled": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.retried"] + "enum": ["session.retry.scheduled"] }, "durable": { "type": "object", @@ -29419,55 +17205,90 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, "attempt": { - "type": "number" + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "at": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "error": { - "$ref": "#/components/schemas/SessionNextRetry_error" + "$ref": "#/components/schemas/Session.StructuredError" } }, - "required": ["timestamp", "sessionID", "attempt", "error"], + "required": ["sessionID", "assistantMessageID", "attempt", "at", "error"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextCompactionStarted": { + "session.compaction.started": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.compaction.started"] + "enum": ["session.compaction.started"] }, "durable": { "type": "object", @@ -29476,57 +17297,78 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "reason": { "type": "string", "enum": ["auto", "manual"] + }, + "recent": { + "type": "string" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, - "required": ["timestamp", "sessionID", "messageID", "reason"], + "required": ["sessionID", "reason", "recent"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextCompactionEnded": { + "session.compaction.ended": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.compaction.ended"] + "enum": ["session.compaction.ended"] }, "durable": { "type": "object", @@ -29535,31 +17377,34 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^ses" + } + ] }, "reason": { "type": "string", @@ -29572,26 +17417,33 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], + "required": ["sessionID", "reason", "text", "recent"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextRevertStaged": { + "session.compaction.failed": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.revert.staged"] + "enum": ["session.compaction.failed"] }, "durable": { "type": "object", @@ -29600,52 +17452,78 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "revert": { - "$ref": "#/components/schemas/RevertState" + "reason": { + "type": "string", + "enum": ["auto", "manual"] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, - "required": ["timestamp", "sessionID", "revert"], + "required": ["sessionID", "reason", "error"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextRevertCleared": { + "session.revert.staged": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.revert.cleared"] + "enum": ["session.revert.staged"] }, "durable": { "type": "object", @@ -29654,49 +17532,66 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "revert": { + "$ref": "#/components/schemas/Session.Revert" } }, - "required": ["timestamp", "sessionID"], + "required": ["sessionID", "revert"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "SessionNextRevertCommitted": { + "session.revert.cleared": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.revert.committed"] + "enum": ["session.revert.cleared"] }, "durable": { "type": "object", @@ -29705,3973 +17600,3787 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - } - }, - "required": ["timestamp", "sessionID", "messageID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "ModelApi": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["aisdk"] - }, - "package": { - "type": "string" - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" - } - }, - "required": ["id", "type", "package"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["native"] - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" + "allOf": [ + { + "pattern": "^ses" + } + ] } }, - "required": ["id", "type", "settings"], + "required": ["sessionID"], "additionalProperties": false } - ] - }, - "ModelCapabilities": { - "type": "object", - "properties": { - "tools": { - "type": "boolean" - }, - "input": { - "type": "array", - "items": { - "type": "string" - } - }, - "output": { - "type": "array", - "items": { - "type": "string" - } - } }, - "required": ["tools", "input", "output"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "ModelCost": { + "session.revert.committed": { "type": "object", "properties": { - "tier": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["context"] - }, - "size": { - "type": "integer" + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" } - }, - "required": ["type", "size"], - "additionalProperties": false - }, - "input": { - "type": "number" + ] }, - "output": { + "created": { "type": "number" }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "cache"], - "additionalProperties": false - }, - "ModelV2Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "family": { - "type": "string" - }, - "name": { - "type": "string" - }, - "api": { - "$ref": "#/components/schemas/ModelApi" + "metadata": { + "type": "object" }, - "capabilities": { - "$ref": "#/components/schemas/ModelCapabilities" + "type": { + "type": "string", + "enum": ["session.revert.committed"] }, - "request": { + "durable": { "type": "object", "properties": { - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" - }, - "variant": { + "aggregateID": { "type": "string" - } - }, - "required": ["headers", "body"], - "additionalProperties": false - }, - "variants": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "body": { - "type": "object" - } + ] }, - "required": ["id", "headers", "body"], - "additionalProperties": false - } - }, - "time": { - "type": "object", - "properties": { - "released": { - "type": "number" + "version": { + "type": "number", + "enum": [1] } }, - "required": ["released"], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, - "cost": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelCost" - } - }, - "status": { - "type": "string", - "enum": ["alpha", "beta", "deprecated", "active"] - }, - "enabled": { - "type": "boolean" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "limit": { + "data": { "type": "object", "properties": { - "context": { - "type": "integer" - }, - "input": { - "type": "integer" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "output": { - "type": "integer" + "to": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, - "required": ["context", "output"], + "required": ["sessionID", "to"], "additionalProperties": false } }, - "required": [ - "id", - "providerID", - "name", - "api", - "capabilities", - "request", - "variants", - "time", - "cost", - "status", - "enabled", - "limit" - ], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "ProviderAISDK": { + "session.usage.recorded": { "type": "object", "properties": { - "type": { + "id": { "type": "string", - "enum": ["aisdk"] - }, - "package": { - "type": "string" + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "url": { - "type": "string" + "created": { + "type": "number" }, - "settings": { + "metadata": { "type": "object" - } - }, - "required": ["type", "package"], - "additionalProperties": false - }, - "ProviderNative": { - "type": "object", - "properties": { + }, "type": { "type": "string", - "enum": ["native"] + "enum": ["session.usage.recorded"] }, - "url": { - "type": "string" + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [1] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, - "settings": { - "type": "object" + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "source": { + "type": "string", + "enum": ["title", "compaction"] + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" + } + }, + "required": ["sessionID", "source", "cost", "tokens"], + "additionalProperties": false } }, - "required": ["type", "settings"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "ProviderApi": { - "anyOf": [ + "Session.Event.Durable": { + "oneOf": [ { - "$ref": "#/components/schemas/ProviderAISDK" + "$ref": "#/components/schemas/session.created" }, { - "$ref": "#/components/schemas/ProviderNative" - } - ] - }, - "ProviderV2Info": { - "type": "object", - "properties": { - "id": { - "type": "string" + "$ref": "#/components/schemas/session.agent.selected" }, - "integrationID": { - "type": "string" + { + "$ref": "#/components/schemas/session.model.selected" }, - "name": { - "type": "string" + { + "$ref": "#/components/schemas/session.moved" }, - "disabled": { - "type": "boolean" + { + "$ref": "#/components/schemas/session.renamed" }, - "api": { - "$ref": "#/components/schemas/ProviderApi" + { + "$ref": "#/components/schemas/session.deleted" }, - "request": { - "$ref": "#/components/schemas/ProviderRequest" - } - }, - "required": ["id", "name", "api", "request"], - "additionalProperties": false - }, - "IntegrationWhen": { - "type": "object", - "properties": { - "key": { - "type": "string" + { + "$ref": "#/components/schemas/session.forked" }, - "op": { - "type": "string", - "enum": ["eq", "neq"] + { + "$ref": "#/components/schemas/session.inbox.delivered" }, - "value": { - "type": "string" - } - }, - "required": ["key", "op", "value"], - "additionalProperties": false - }, - "IntegrationTextPrompt": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["text"] + { + "$ref": "#/components/schemas/session.inbox.enqueued" }, - "key": { - "type": "string" + { + "$ref": "#/components/schemas/session.inbox.cancelled" }, - "message": { - "type": "string" + { + "$ref": "#/components/schemas/session.inbox.delivery.changed" }, - "placeholder": { - "type": "string" + { + "$ref": "#/components/schemas/session.execution.started" }, - "when": { - "$ref": "#/components/schemas/IntegrationWhen" - } - }, - "required": ["type", "key", "message"], - "additionalProperties": false - }, - "IntegrationSelectPrompt": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["select"] + { + "$ref": "#/components/schemas/session.execution.succeeded" }, - "key": { - "type": "string" + { + "$ref": "#/components/schemas/session.execution.failed" }, - "message": { - "type": "string" + { + "$ref": "#/components/schemas/session.execution.interrupted" }, - "options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "value": { - "type": "string" - }, - "hint": { - "type": "string" - } - }, - "required": ["label", "value"], - "additionalProperties": false - } + { + "$ref": "#/components/schemas/session.instructions.updated" }, - "when": { - "$ref": "#/components/schemas/IntegrationWhen" - } - }, - "required": ["type", "key", "message", "options"], - "additionalProperties": false - }, - "IntegrationOAuthMethod": { - "type": "object", - "properties": { - "id": { - "type": "string" + { + "$ref": "#/components/schemas/session.synthetic" }, - "type": { - "type": "string", - "enum": ["oauth"] + { + "$ref": "#/components/schemas/session.skill.activated" }, - "label": { - "type": "string" + { + "$ref": "#/components/schemas/session.shell.started" }, - "prompts": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/IntegrationTextPrompt" - }, - { - "$ref": "#/components/schemas/IntegrationSelectPrompt" - } - ] - } - } - }, - "required": ["id", "type", "label"], - "additionalProperties": false - }, - "IntegrationKeyMethod": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["key"] + { + "$ref": "#/components/schemas/session.shell.ended" + }, + { + "$ref": "#/components/schemas/session.step.started" + }, + { + "$ref": "#/components/schemas/session.step.ended" + }, + { + "$ref": "#/components/schemas/session.step.failed" + }, + { + "$ref": "#/components/schemas/session.text.started" + }, + { + "$ref": "#/components/schemas/session.text.ended" }, - "label": { - "type": "string" - } - }, - "required": ["type"], - "additionalProperties": false - }, - "IntegrationEnvMethod": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["env"] + { + "$ref": "#/components/schemas/session.reasoning.started" }, - "names": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["type", "names"], - "additionalProperties": false - }, - "ConnectionCredentialInfo": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["credential"] + { + "$ref": "#/components/schemas/session.reasoning.ended" }, - "id": { - "type": "string" + { + "$ref": "#/components/schemas/session.tool.input.started" }, - "label": { - "type": "string" - } - }, - "required": ["type", "id", "label"], - "additionalProperties": false - }, - "ConnectionEnvInfo": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["env"] + { + "$ref": "#/components/schemas/session.tool.input.ended" }, - "name": { - "type": "string" - } - }, - "required": ["type", "name"], - "additionalProperties": false - }, - "ConnectionInfo": { - "anyOf": [ { - "$ref": "#/components/schemas/ConnectionCredentialInfo" + "$ref": "#/components/schemas/session.tool.called" }, { - "$ref": "#/components/schemas/ConnectionEnvInfo" - } - ] - }, - "IntegrationInfo": { - "type": "object", - "properties": { - "id": { - "type": "string" + "$ref": "#/components/schemas/session.tool.success" }, - "name": { - "type": "string" + { + "$ref": "#/components/schemas/session.tool.failed" }, - "methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IntegrationMethod" - } + { + "$ref": "#/components/schemas/session.retry.scheduled" }, - "connections": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ConnectionInfo" - } + { + "$ref": "#/components/schemas/session.compaction.started" + }, + { + "$ref": "#/components/schemas/session.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.compaction.failed" + }, + { + "$ref": "#/components/schemas/session.revert.staged" + }, + { + "$ref": "#/components/schemas/session.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.revert.committed" + }, + { + "$ref": "#/components/schemas/session.usage.recorded" } - }, - "required": ["id", "name", "methods", "connections"], - "additionalProperties": false + ] }, - "IntegrationAttempt": { + "EventLog.Synced": { "type": "object", "properties": { - "attemptID": { - "type": "string" - }, - "url": { - "type": "string" + "type": { + "type": "string", + "enum": ["log.synced"] }, - "instructions": { + "aggregateID": { "type": "string" }, - "mode": { - "type": "string", - "enum": ["auto", "code"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": ["created", "expires"], - "additionalProperties": false + ] } }, - "required": ["attemptID", "url", "instructions", "mode", "time"], - "additionalProperties": false + "required": ["type", "aggregateID"], + "additionalProperties": false, + "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." }, - "IntegrationAttemptStatus": { + "SessionLogItem": { "anyOf": [ { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["pending"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["created", "expires"], - "additionalProperties": false - } - }, - "required": ["status", "time"], - "additionalProperties": false + "$ref": "#/components/schemas/Session.Event.Durable" }, { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["complete"] - }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] + "$ref": "#/components/schemas/EventLog.Synced" + } + ] + }, + "SessionLogItemJsonString": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/SessionLogItem" + }, + "contentMediaType": "application/json" + }, + "SessionMessagesResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message.Info" + } + }, + "cursor": { + "type": "object", + "properties": { + "previous": { + "anyOf": [ + { + "type": "string" }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] + { + "type": "null" } - }, - "required": ["created", "expires"], - "additionalProperties": false + ] + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, - "required": ["status", "time"], "additionalProperties": false + } + }, + "required": ["data", "cursor"], + "additionalProperties": false + }, + "Model.ReasoningField": { + "anyOf": [ + { + "type": "string", + "enum": ["reasoning", "reasoning_content", "reasoning_text"] }, { + "type": "string" + } + ] + }, + "Model.MaxTokensField": { + "type": "string", + "enum": ["max_completion_tokens", "max_tokens"] + }, + "Model.Compatibility": { + "type": "object", + "properties": { + "reasoningField": { + "$ref": "#/components/schemas/Model.ReasoningField" + }, + "maxTokensField": { + "$ref": "#/components/schemas/Model.MaxTokensField" + }, + "requireFinishReason": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Model.Capabilities": { + "type": "object", + "properties": { + "tools": { + "type": "boolean" + }, + "input": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["tools", "input", "output"], + "additionalProperties": false + }, + "Model.Variant": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "Money.USDPerMillionTokens": { + "type": "number" + }, + "Model.Cost": { + "type": "object", + "properties": { + "tier": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "enum": ["failed"] - }, - "message": { - "type": "string" + "enum": ["context"] }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["created", "expires"], - "additionalProperties": false + "size": { + "type": "integer" } }, - "required": ["status", "message", "time"], + "required": ["type", "size"], "additionalProperties": false }, - { + "input": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "output": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "cache": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": ["expired"] + "read": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, - "time": { - "type": "object", - "properties": { - "created": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - }, - "expires": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - }, - { - "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] - } - ] - } - }, - "required": ["created", "expires"], - "additionalProperties": false + "write": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" } }, - "required": ["status", "time"], + "required": ["read", "write"], "additionalProperties": false } - ] + }, + "required": ["input", "output", "cache"], + "additionalProperties": false }, - "PermissionV2Request": { + "Model.Info": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^per" + "type": "string" }, - "sessionID": { - "type": "string", - "pattern": "^ses" + "modelID": { + "type": "string" }, - "action": { + "providerID": { "type": "string" }, - "resources": { - "type": "array", - "items": { + "family": { + "type": "string" + }, + "name": { + "type": "string" + }, + "compatibility": { + "$ref": "#/components/schemas/Model.Compatibility" + }, + "package": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { "type": "string" } }, - "save": { + "body": { + "type": "object" + }, + "capabilities": { + "$ref": "#/components/schemas/Model.Capabilities" + }, + "variants": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/Model.Variant" } }, - "metadata": { - "type": "object" + "time": { + "type": "object", + "properties": { + "released": { + "type": "number" + } + }, + "required": ["released"], + "additionalProperties": false }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - } - }, - "required": ["id", "sessionID", "action", "resources"], - "additionalProperties": false - }, - "PermissionSavedInfo": { - "type": "object", - "properties": { - "id": { - "type": "string" + "cost": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Cost" + } }, - "projectID": { - "type": "string" + "status": { + "type": "string", + "enum": ["alpha", "beta", "deprecated", "active"] }, - "action": { - "type": "string" + "enabled": { + "type": "boolean" }, - "resource": { - "type": "string" + "limit": { + "type": "object", + "properties": { + "context": { + "type": "integer" + }, + "input": { + "type": "integer" + }, + "output": { + "type": "integer" + } + }, + "required": ["context", "output"], + "additionalProperties": false } }, - "required": ["id", "projectID", "action", "resource"], + "required": [ + "id", + "modelID", + "providerID", + "name", + "capabilities", + "variants", + "time", + "cost", + "status", + "enabled", + "limit" + ], "additionalProperties": false }, - "FileSystemEntry": { + "GenerateTextResponse": { "type": "object", "properties": { - "path": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["file", "directory"] + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"], + "additionalProperties": false } }, - "required": ["path", "type"], + "required": ["data"], "additionalProperties": false }, - "CommandV2Info": { + "Provider.Info": { "type": "object", "properties": { - "name": { + "id": { "type": "string" }, - "template": { + "integrationID": { "type": "string" }, - "description": { + "name": { "type": "string" }, - "agent": { + "disabled": { + "type": "boolean" + }, + "package": { "type": "string" }, - "model": { - "$ref": "#/components/schemas/ModelRef" + "settings": { + "type": "object" }, - "subtask": { - "type": "boolean" + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" } }, - "required": ["name", "template"], + "required": ["id", "name", "package"], "additionalProperties": false }, - "SkillV2Info": { + "ProviderNotFoundError": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "slash": { - "type": "boolean" + "_tag": { + "type": "string", + "enum": ["ProviderNotFoundError"] }, - "location": { + "providerID": { "type": "string" }, - "content": { + "message": { "type": "string" } }, - "required": ["name", "location", "content"], + "required": ["_tag", "providerID", "message"], "additionalProperties": false }, - "Models-devRefreshed": { + "Form.When": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" + "key": { + "type": "string" }, - "type": { + "op": { "type": "string", - "enum": ["models-dev.refreshed"] + "enum": ["eq", "neq"] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { + "value": { + "anyOf": [ + { "type": "string" }, - "seq": { - "type": "integer" + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] }, - "version": { - "type": "integer" + { + "type": "boolean" } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + ] + } + }, + "required": ["key", "op", "value"], + "additionalProperties": false + }, + "Form.Option": { + "type": "object", + "properties": { + "value": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "label": { + "type": "string" }, - "data": { - "type": "object", - "properties": {} + "description": { + "type": "string" } }, - "required": ["id", "type", "data"], + "required": ["value", "label"], "additionalProperties": false }, - "IntegrationUpdated": { + "Form.StringField": { "type": "object", "properties": { - "id": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": ["string"] + }, + "format": { "type": "string", - "pattern": "^evt_" + "enum": ["email", "uri", "date", "date-time"] + }, + "minLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "metadata": { - "type": "object" + "maxLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "type": { - "type": "string", - "enum": ["integration.updated"] + "pattern": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "placeholder": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "default": { + "type": "string" }, - "data": { - "type": "object", - "properties": {} + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "custom": { + "type": "boolean" } }, - "required": ["id", "type", "data"], + "required": ["key", "type"], "additionalProperties": false }, - "IntegrationConnectionUpdated": { + "Form.NumberField": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "key": { + "type": "string" }, - "metadata": { - "type": "object" + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } }, "type": { "type": "string", - "enum": ["integration.connection.updated"] + "enum": ["number"] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" + "minimum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] }, - "version": { - "type": "integer" + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + ] }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "maximum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] }, - "data": { - "type": "object", - "properties": { - "integrationID": { - "type": "string" + "default": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] } - }, - "required": ["integrationID"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "data"], + "required": ["key", "type"], "additionalProperties": false }, - "CatalogUpdated": { + "Form.IntegerField": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "key": { + "type": "string" }, - "metadata": { - "type": "object" + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } }, "type": { "type": "string", - "enum": ["catalog.updated"] + "enum": ["integer"] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" + "minimum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] }, - "version": { - "type": "integer" + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + ] }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "maximum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] }, - "data": { - "type": "object", - "properties": {} + "default": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] } }, - "required": ["id", "type", "data"], + "required": ["key", "type"], "additionalProperties": false }, - "SessionCreated": { + "Form.BooleanField": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "key": { + "type": "string" }, - "metadata": { - "type": "object" + "title": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["session.created"] + "description": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "required": { + "type": "boolean" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false + "type": { + "type": "string", + "enum": ["boolean"] + }, + "default": { + "type": "boolean" } }, - "required": ["id", "type", "data"], + "required": ["key", "type"], "additionalProperties": false }, - "SessionUpdated": { + "Form.MultiselectField": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "key": { + "type": "string" }, - "metadata": { - "type": "object" + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } }, "type": { "type": "string", - "enum": ["session.updated"] + "enum": ["multiselect"] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "minItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" + "maxItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": ["sessionID", "info"], - "additionalProperties": false + ] + }, + "custom": { + "type": "boolean" + }, + "default": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": ["id", "type", "data"], + "required": ["key", "type", "options"], "additionalProperties": false }, - "SessionDeleted": { + "Form.ExternalField": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" + "key": { + "type": "string" }, "type": { "type": "string", - "enum": ["session.deleted"] + "enum": ["external"] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "url": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "title": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false + "description": { + "type": "string" } }, - "required": ["id", "type", "data"], + "required": ["key", "type", "url"], "additionalProperties": false }, - "MessageUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "Form.Field": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField" }, - "metadata": { - "type": "object" + { + "$ref": "#/components/schemas/Form.NumberField" }, - "type": { - "type": "string", - "enum": ["message.updated"] + { + "$ref": "#/components/schemas/Form.IntegerField" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + { + "$ref": "#/components/schemas/Form.BooleanField" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + { + "$ref": "#/components/schemas/Form.MultiselectField" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Message" - } - }, - "required": ["sessionID", "info"], - "additionalProperties": false + { + "$ref": "#/components/schemas/Form.ExternalField" } - }, - "required": ["id", "type", "data"], - "additionalProperties": false + ] + }, + "Form.Fields": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field" + } }, - "MessageRemoved": { + "Integration.OAuthMethod": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" + "type": "string" }, "type": { - "type": "string", - "enum": ["message.removed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - } - }, - "required": ["sessionID", "messageID"], - "additionalProperties": false + "type": "string", + "enum": ["oauth"] + }, + "label": { + "type": "string" + }, + "form": { + "$ref": "#/components/schemas/Form.Fields" } }, - "required": ["id", "type", "data"], + "required": ["id", "type", "label"], "additionalProperties": false }, - "MessagePartUpdated": { + "Integration.CommandMethod": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" + "type": "string" }, "type": { "type": "string", - "enum": ["message.part.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["command"] }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "label": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "part": { - "$ref": "#/components/schemas/Part" - }, - "time": { - "type": "number" - } - }, - "required": ["sessionID", "part", "time"], - "additionalProperties": false + "command": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": ["id", "type", "data"], + "required": ["id", "type", "label", "command"], "additionalProperties": false }, - "MessagePartRemoved": { + "Integration.KeyMethod": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, "type": { "type": "string", - "enum": ["message.part.removed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["key"] }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "label": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } - }, - "required": ["sessionID", "messageID", "partID"], - "additionalProperties": false + "form": { + "$ref": "#/components/schemas/Form.Fields" } }, - "required": ["id", "type", "data"], + "required": ["type"], "additionalProperties": false }, - "SessionNextTextDelta": { + "Integration.EnvMethod": { "type": "object", "properties": { - "id": { + "type": { "type": "string", - "pattern": "^evt_" + "enum": ["env"] }, - "metadata": { - "type": "object" + "names": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["type", "names"], + "additionalProperties": false + }, + "Integration.Method": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.OAuthMethod" + }, + { + "$ref": "#/components/schemas/Integration.CommandMethod" }, + { + "$ref": "#/components/schemas/Integration.KeyMethod" + }, + { + "$ref": "#/components/schemas/Integration.EnvMethod" + } + ] + }, + "Connection.CredentialInfo": { + "type": "object", + "properties": { "type": { "type": "string", - "enum": ["session.next.text.delta"] + "enum": ["credential"] }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "id": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "label": { + "type": "string" + } + }, + "required": ["type", "id", "label"], + "additionalProperties": false + }, + "Connection.EnvInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["env"] }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], - "additionalProperties": false + "name": { + "type": "string" } }, - "required": ["id", "type", "data"], + "required": ["type", "name"], "additionalProperties": false }, - "SessionNextReasoningDelta": { + "Connection.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Connection.CredentialInfo" + }, + { + "$ref": "#/components/schemas/Connection.EnvInfo" + } + ] + }, + "Integration.Info": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^evt_" + "type": "string" }, - "metadata": { - "type": "object" + "name": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.delta"] + "methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Method" + } }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" + "connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connection.Info" + } + } + }, + "required": ["id", "name", "methods", "connections"], + "additionalProperties": false + }, + "Form.Value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] }, - "version": { - "type": "integer" + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + ] + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "Form.Answer": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Form.Value" + } + }, + "Integration.Attempt": { + "type": "object", + "properties": { + "attemptID": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "url": { + "type": "string" }, - "data": { + "instructions": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": ["auto", "code"] + }, + "time": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] }, - "delta": { - "type": "string" + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["attemptID", "url", "instructions", "mode", "time"], "additionalProperties": false }, - "SessionNextToolInputDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.tool.input.delta"] - }, - "durable": { + "Integration.AttemptStatus": { + "anyOf": [ + { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" + "status": { + "type": "string", + "enum": ["pending"] }, - "version": { - "type": "integer" + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false } }, - "required": ["aggregateID", "seq", "version"], + "required": ["status", "time"], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { + { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { + "status": { "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" + "enum": ["complete"] }, - "delta": { - "type": "string" + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionNextCompactionDelta": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.compaction.delta"] + "required": ["status", "time"], + "additionalProperties": false }, - "durable": { + { "type": "object", "properties": { - "aggregateID": { - "type": "string" + "status": { + "type": "string", + "enum": ["failed"] }, - "seq": { - "type": "integer" + "message": { + "type": "string" }, - "version": { - "type": "integer" + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false } }, - "required": ["aggregateID", "seq", "version"], + "required": ["status", "message", "time"], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { + { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { + "status": { "type": "string", - "pattern": "^msg_" + "enum": ["expired"] }, - "text": { - "type": "string" + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "messageID", "text"], + "required": ["status", "time"], "additionalProperties": false } - }, - "required": ["id", "type", "data"], - "additionalProperties": false + ] }, - "MessagePartDelta": { + "Integration.CommandAttempt": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["message.part.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "attemptID": { + "type": "string" }, - "data": { + "time": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "field": { - "type": "string" + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] }, - "delta": { - "type": "string" + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] } }, - "required": ["sessionID", "messageID", "partID", "field", "delta"], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["attemptID", "time"], "additionalProperties": false }, - "SessionDiff": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.diff"] - }, - "durable": { + "Integration.CommandAttemptStatus": { + "anyOf": [ + { "type": "object", "properties": { - "aggregateID": { - "type": "string" + "status": { + "type": "string", + "enum": ["pending"] }, - "seq": { - "type": "integer" + "message": { + "type": "string" }, - "version": { - "type": "integer" + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false } }, - "required": ["aggregateID", "seq", "version"], + "required": ["status", "time"], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { + { "type": "object", "properties": { - "sessionID": { + "status": { "type": "string", - "pattern": "^ses" + "enum": ["complete"] }, - "diff": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false } }, - "required": ["sessionID", "diff"], + "required": ["status", "time"], "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "SessionError": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.error"] }, - "durable": { + { "type": "object", "properties": { - "aggregateID": { - "type": "string" + "status": { + "type": "string", + "enum": ["failed"] }, - "seq": { - "type": "integer" + "message": { + "type": "string" }, - "version": { - "type": "integer" + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false } }, - "required": ["aggregateID", "seq", "version"], + "required": ["status", "message", "time"], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { + { "type": "object", "properties": { - "sessionID": { + "status": { "type": "string", - "pattern": "^ses" + "enum": ["expired"] }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/StructuredOutputError" - }, - { - "$ref": "#/components/schemas/ContextOverflowError" - }, - { - "$ref": "#/components/schemas/ContentFilterError" + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] }, - { - "$ref": "#/components/schemas/APIError" + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] } - ] + }, + "required": ["created", "expires"], + "additionalProperties": false } }, + "required": ["status", "time"], "additionalProperties": false } - }, - "required": ["id", "type", "data"], - "additionalProperties": false + ] }, - "InstallationUpdated": { + "Mcp.Status.Connected": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { + "status": { "type": "string", - "enum": ["installation.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false + "enum": ["connected"] } }, - "required": ["id", "type", "data"], + "required": ["status"], "additionalProperties": false }, - "InstallationUpdate-available": { + "Mcp.Status.Pending": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { + "status": { "type": "string", - "enum": ["installation.update-available"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false + "enum": ["pending"] } }, - "required": ["id", "type", "data"], + "required": ["status"], "additionalProperties": false }, - "FileEdited": { + "Mcp.Status.Disabled": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { + "status": { "type": "string", - "enum": ["file.edited"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"], - "additionalProperties": false + "enum": ["disabled"] } }, - "required": ["id", "type", "data"], + "required": ["status"], "additionalProperties": false }, - "ReferenceUpdated": { + "Mcp.Status.Failed": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { + "status": { "type": "string", - "enum": ["reference.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "enum": ["failed"] }, - "data": { - "type": "object", - "properties": {} + "error": { + "type": "string" } }, - "required": ["id", "type", "data"], + "required": ["status", "error"], "additionalProperties": false }, - "PermissionV2Asked": { + "Mcp.Status.NeedsAuth": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["permission.v2.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - } - }, - "required": ["id", "sessionID", "action", "resources"], - "additionalProperties": false + "status": { + "type": "string", + "enum": ["needs_auth"] } }, - "required": ["id", "type", "data"], + "required": ["status"], "additionalProperties": false }, - "PermissionV2Replied": { + "Mcp.Server": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["permission.v2.replied"] + "name": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.Status.Connected" }, - "seq": { - "type": "integer" + { + "$ref": "#/components/schemas/Mcp.Status.Pending" }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" + { + "$ref": "#/components/schemas/Mcp.Status.Disabled" }, - "requestID": { - "type": "string", - "pattern": "^per" + { + "$ref": "#/components/schemas/Mcp.Status.Failed" }, - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" + { + "$ref": "#/components/schemas/Mcp.Status.NeedsAuth" } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false + ] + }, + "integrationID": { + "type": "string" } }, - "required": ["id", "type", "data"], + "required": ["name", "status"], "additionalProperties": false }, - "PluginAdded": { + "Mcp.LocalConfig": { "type": "object", "properties": { - "id": { + "type": { "type": "string", - "pattern": "^evt_" + "enum": ["local"] }, - "metadata": { - "type": "object" + "command": { + "type": "array", + "items": { + "type": "string" + } }, - "type": { - "type": "string", - "enum": ["plugin.added"] + "cwd": { + "type": "string" }, - "durable": { + "environment": { "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "additionalProperties": { + "type": "string" + } }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "disabled": { + "type": "boolean" }, - "data": { + "codemode": { + "type": "boolean" + }, + "timeout": { "type": "object", "properties": { - "id": { - "type": "string" + "startup": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "catalog": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "execution": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] } }, - "required": ["id"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["type", "command"], "additionalProperties": false }, - "ProjectDirectoriesUpdated": { + "Mcp.OAuthConfig": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "client_id": { + "type": "string" }, - "metadata": { - "type": "object" + "client_secret": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["project.directories.updated"] + "scope": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" + "callback_port": { + "type": "integer", + "allOf": [ + { + "minimum": 1, + "maximum": 65535 } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" + ] }, - "data": { - "type": "object", - "properties": { - "projectID": { - "type": "string" - } - }, - "required": ["projectID"], - "additionalProperties": false + "redirect_uri": { + "type": "string" } }, - "required": ["id", "type", "data"], "additionalProperties": false }, - "FileWatcherUpdated": { + "Mcp.RemoteConfig": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, "type": { "type": "string", - "enum": ["file.watcher.updated"] + "enum": ["remote"] }, - "durable": { + "url": { + "type": "string" + }, + "headers": { "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" + "additionalProperties": { + "type": "string" + } + }, + "oauth": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.OAuthConfig" }, - "version": { - "type": "integer" + { + "type": "boolean", + "enum": [false] } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + ] }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "disabled": { + "type": "boolean" }, - "data": { + "codemode": { + "type": "boolean" + }, + "timeout": { "type": "object", "properties": { - "file": { - "type": "string" + "startup": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] }, - "event": { - "type": "string", - "enum": ["add", "change", "unlink"] + "catalog": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "execution": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] } }, - "required": ["file", "event"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["type", "url"], "additionalProperties": false }, - "PtyCreated": { + "McpServerNotFoundError": { "type": "object", "properties": { - "id": { + "_tag": { "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" + "enum": ["McpServerNotFoundError"] }, - "type": { - "type": "string", - "enum": ["pty.created"] + "server": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "message": { + "type": "string" + } + }, + "required": ["_tag", "server", "message"], + "additionalProperties": false + }, + "Mcp.Resource": { + "type": "object", + "properties": { + "server": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "name": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false + "uri": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mimeType": { + "type": "string" } }, - "required": ["id", "type", "data"], + "required": ["server", "name", "uri"], "additionalProperties": false }, - "PtyUpdated": { + "Mcp.ResourceTemplate": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "server": { + "type": "string" }, - "metadata": { - "type": "object" + "name": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["pty.updated"] + "uriTemplate": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "description": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "mimeType": { + "type": "string" + } + }, + "required": ["server", "name", "uriTemplate"], + "additionalProperties": false + }, + "Mcp.ResourceCatalog": { + "type": "object", + "properties": { + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.Resource" + } }, - "data": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false + "templates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.ResourceTemplate" + } } }, - "required": ["id", "type", "data"], + "required": ["resources", "templates"], "additionalProperties": false }, - "PtyExited": { + "Project.Vcs": { + "type": "string", + "enum": ["git", "hg"] + }, + "Project.Icon": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "url": { + "type": "string" }, - "metadata": { - "type": "object" + "override": { + "type": "string" }, - "type": { + "color": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Project.Commands": { + "type": "object", + "properties": { + "start": { "type": "string", - "enum": ["pty.exited"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" + "description": "Startup script to run when creating a new workspace (worktree)" + } + }, + "additionalProperties": false + }, + "Project.Time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + ] }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "updated": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - }, - "exitCode": { - "type": "integer", + "initialized": { + "type": "integer", + "allOf": [ + { "minimum": 0 } - }, - "required": ["id", "exitCode"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "data"], + "required": ["created", "updated"], "additionalProperties": false }, - "PtyDeleted": { + "Project": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^evt_" + "type": "string" }, - "metadata": { - "type": "object" + "canonical": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["pty.deleted"] + "vcs": { + "$ref": "#/components/schemas/Project.Vcs" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "name": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "icon": { + "$ref": "#/components/schemas/Project.Icon" }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - } - }, - "required": ["id"], - "additionalProperties": false + "commands": { + "$ref": "#/components/schemas/Project.Commands" + }, + "time": { + "$ref": "#/components/schemas/Project.Time" + }, + "sandboxes": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": ["id", "type", "data"], + "required": ["id", "canonical", "time", "sandboxes"], "additionalProperties": false }, - "QuestionV2Asked": { + "Project.Current": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["question.v2.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "directory": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Info" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionV2Tool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false + "canonical": { + "type": "string" + } + }, + "required": ["id", "directory", "canonical"], + "additionalProperties": false + }, + "Project.Directory": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "strategy": { + "type": "string" } }, - "required": ["id", "type", "data"], + "required": ["directory"], "additionalProperties": false }, - "QuestionV2Replied": { + "Project.Directories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project.Directory" + } + }, + "Form.Metadata": { + "type": "object" + }, + "Form.Info": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" - }, - "metadata": { - "type": "object" + "allOf": [ + { + "pattern": "^frm_" + } + ] }, - "type": { - "type": "string", - "enum": ["question.v2.replied"] + "sessionID": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "title": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Answer" - } - } - }, - "required": ["sessionID", "requestID", "answers"], - "additionalProperties": false + "fields": { + "$ref": "#/components/schemas/Form.Fields" } }, - "required": ["id", "type", "data"], + "required": ["id", "sessionID", "title", "fields"], "additionalProperties": false }, - "QuestionV2Rejected": { + "Form.CreatePayload": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^evt_" + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + { + "type": "null" + } + ] + }, + "title": { + "type": "string" }, "metadata": { - "type": "object" + "$ref": "#/components/schemas/Form.Metadata" }, - "type": { + "fields": { + "$ref": "#/components/schemas/Form.Fields" + } + }, + "required": ["title", "fields"], + "additionalProperties": false + }, + "FormNotFoundError": { + "type": "object", + "properties": { + "_tag": { "type": "string", - "enum": ["question.v2.rejected"] + "enum": ["FormNotFoundError"] }, - "durable": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "id", "message"], + "additionalProperties": false + }, + "Form.State": { + "anyOf": [ + { "type": "object", "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" + "status": { + "type": "string", + "enum": ["pending"] } }, - "required": ["aggregateID", "seq", "version"], + "required": ["status"], "additionalProperties": false }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { + { "type": "object", "properties": { - "sessionID": { + "status": { "type": "string", - "pattern": "^ses" + "enum": ["answered"] }, - "requestID": { + "answer": { + "$ref": "#/components/schemas/Form.Answer" + } + }, + "required": ["status", "answer"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { "type": "string", - "pattern": "^que" + "enum": ["cancelled"] } }, - "required": ["sessionID", "requestID"], + "required": ["status"], "additionalProperties": false } + ] + }, + "Form.Reply": { + "type": "object", + "properties": { + "answer": { + "$ref": "#/components/schemas/Form.Answer" + } }, - "required": ["id", "type", "data"], + "required": ["answer"], "additionalProperties": false }, - "TodoUpdated": { + "FormAlreadySettledError": { "type": "object", "properties": { - "id": { + "_tag": { "type": "string", - "pattern": "^evt_" + "enum": ["FormAlreadySettledError"] }, - "metadata": { - "type": "object" + "id": { + "type": "string" }, - "type": { + "message": { + "type": "string" + } + }, + "required": ["_tag", "id", "message"], + "additionalProperties": false + }, + "FormInvalidAnswerError": { + "type": "object", + "properties": { + "_tag": { "type": "string", - "enum": ["todo.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["FormInvalidAnswerError"] }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "id": { + "type": "string" }, - "data": { + "message": { + "type": "string" + } + }, + "required": ["_tag", "id", "message"], + "additionalProperties": false + }, + "Permission.Source": { + "anyOf": [ + { "type": "object", "properties": { - "sessionID": { + "type": { "type": "string", - "pattern": "^ses" + "enum": ["tool"] }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } + "messageID": { + "type": "string" + }, + "id": { + "type": "string" } }, - "required": ["sessionID", "todos"], + "required": ["type", "messageID", "id"], "additionalProperties": false } - }, - "required": ["id", "type", "data"], - "additionalProperties": false + ] }, - "LspUpdated": { + "Permission.Request": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } }, "metadata": { "type": "object" }, - "type": { - "type": "string", - "enum": ["lsp.updated"] + "source": { + "$ref": "#/components/schemas/Permission.Source" + } + }, + "required": ["id", "sessionID", "action", "resources"], + "additionalProperties": false + }, + "PermissionSaved.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "projectID": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "action": { + "type": "string" }, - "data": { - "type": "object", - "properties": {} + "resource": { + "type": "string" } }, - "required": ["id", "type", "data"], + "required": ["id", "projectID", "action", "resource"], "additionalProperties": false }, - "PermissionAsked": { + "PermissionNotFoundError": { "type": "object", "properties": { - "id": { + "_tag": { "type": "string", - "pattern": "^evt_" + "enum": ["PermissionNotFoundError"] }, - "metadata": { - "type": "object" + "requestID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "requestID", "message"], + "additionalProperties": false + }, + "Permission.Reply": { + "type": "string", + "enum": ["once", "always", "reject"] + }, + "FileSystem.Entry": { + "type": "object", + "properties": { + "path": { + "type": "string" }, "type": { "type": "string", - "enum": ["permission.asked"] + "enum": ["file", "directory"] + } + }, + "required": ["path", "type"], + "additionalProperties": false + }, + "Command.Info": { + "type": "object", + "properties": { + "name": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "template": { + "type": "string" }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "description": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"], - "additionalProperties": false - } - }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], - "additionalProperties": false + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "subtask": { + "type": "boolean" } }, - "required": ["id", "type", "data"], + "required": ["name", "template"], "additionalProperties": false }, - "PermissionReplied": { + "Skill.Info": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^evt_" + "type": "string" }, - "metadata": { - "type": "object" + "name": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["permission.replied"] + "description": { + "type": "string" }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "slash": { + "type": "boolean" + }, + "autoinvoke": { + "type": "boolean" }, "location": { - "$ref": "#/components/schemas/LocationRef" + "type": "string" }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "type": "string", - "enum": ["once", "always", "reject"] - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false + "content": { + "type": "string" } }, - "required": ["id", "type", "data"], + "required": ["id", "name", "location", "content"], "additionalProperties": false }, - "TuiPromptAppend": { + "models-dev.refreshed": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["tui.prompt.append"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["models-dev.refreshed"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { - "type": "object", - "properties": { - "text": { - "type": "string" + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" } - }, - "required": ["text"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "TuiCommandExecute": { + "integration.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["tui.command.execute"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["integration.updated"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" } - }, - "required": ["command"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "TuiToastShow": { + "integration.connection.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["tui.toast.show"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["integration.connection.updated"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "title": { - "type": "string" - }, - "message": { + "integrationID": { "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 } }, - "required": ["message", "variant"], + "required": ["integrationID"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "TuiSessionSelect": { + "catalog.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["tui.session.select"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["catalog.updated"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" } - }, - "required": ["sessionID"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "McpToolsChanged": { + "agent.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["mcp.tools.changed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["agent.updated"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { - "type": "object", - "properties": { - "server": { - "type": "string" + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" } - }, - "required": ["server"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "McpBrowserOpenFailed": { + "session.usage.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["mcp.browser.open.failed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["session.usage.updated"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "mcpName": { - "type": "string" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "url": { - "type": "string" + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" } }, - "required": ["mcpName", "url"], + "required": ["sessionID", "cost", "tokens"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "CommandExecuted": { + "session.text.delta": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["command.executed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["session.text.delta"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "name": { - "type": "string" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "arguments": { - "type": "string" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "messageID": { + "assistantMessageID": { "type": "string", - "pattern": "^msg" + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "delta": { + "type": "string" } }, - "required": ["name", "sessionID", "arguments", "messageID"], + "required": ["sessionID", "assistantMessageID", "ordinal", "delta"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "ProjectUpdated": { + "session.reasoning.delta": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["project.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["session.reasoning.delta"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "id": { - "type": "string" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "worktree": { - "type": "string" + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "vcs": { - "$ref": "#/components/schemas/ProjectVcs" + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "name": { + "delta": { "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" - }, - "time": { - "$ref": "#/components/schemas/ProjectTime" - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } } }, - "required": ["id", "worktree", "time", "sandboxes"], + "required": ["sessionID", "assistantMessageID", "ordinal", "delta"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "SessionIdle": { + "session.tool.input.delta": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.idle"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["session.tool.input.delta"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "id": { + "type": "string" + }, + "delta": { + "type": "string" } }, - "required": ["sessionID"], + "required": ["sessionID", "assistantMessageID", "id", "delta"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "QuestionAsked": { + "session.tool.progress": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["question.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["session.tool.progress"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "id": { + "sessionID": { "type": "string", - "pattern": "^que" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "sessionID": { + "assistantMessageID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^msg_" + } + ] }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" + "id": { + "type": "string" }, - "tool": { - "$ref": "#/components/schemas/QuestionTool" + "metadata": { + "type": "object" } }, - "required": ["id", "sessionID", "questions"], + "required": ["sessionID", "assistantMessageID", "id", "metadata"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "SessionCompacted": { + "session.compaction.delta": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.compacted"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["session.compaction.delta"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "text": { + "type": "string" } }, - "required": ["sessionID"], + "required": ["sessionID", "text"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "VcsBranchUpdated": { + "filesystem.changed": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["vcs.branch.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["filesystem.changed"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "branch": { + "file": { "type": "string" + }, + "event": { + "type": "string", + "enum": ["add", "change", "unlink"] } }, + "required": ["file", "event"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "WorkspaceReady": { + "reference.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["workspace.ready"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["reference.updated"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { - "type": "object", - "properties": { - "name": { - "type": "string" + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" } - }, - "required": ["name"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "WorkspaceFailed": { + "permission.asked": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["workspace.failed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["permission.asked"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "message": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "action": { "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/Permission.Source" } }, - "required": ["message"], + "required": ["id", "sessionID", "action", "resources"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "WorkspaceStatus": { + "permission.replied": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["workspace.status"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["permission.replied"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "workspaceID": { + "sessionID": { "type": "string", - "pattern": "^wrk" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "status": { + "requestID": { "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "reply": { + "$ref": "#/components/schemas/Permission.Reply" } }, - "required": ["workspaceID", "status"], + "required": ["sessionID", "requestID", "reply"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "WorktreeReady": { + "plugin.added": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["worktree.ready"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["plugin.added"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "branch": { + "id": { "type": "string" } }, - "required": ["name"], + "required": ["id"], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "WorktreeFailed": { + "plugin.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["worktree.failed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": ["plugin.updated"] }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { - "type": "object", - "properties": { - "message": { - "type": "string" + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" } - }, - "required": ["message"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "ServerConnected": { + "project.directories.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["server.connected"] + "enum": ["project.directories.updated"] }, - "durable": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { "type": "object", "properties": { - "aggregateID": { + "projectID": { "type": "string" - }, - "seq": { - "type": "integer" - }, - "version": { - "type": "integer" } }, - "required": ["aggregateID", "seq", "version"], + "required": ["projectID"], "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/LocationRef" - }, - "data": { - "type": "object", - "properties": {} } }, - "required": ["id", "type", "data"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "GlobalDisposed": { + "project.directory.resolved": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["global.disposed"] + "enum": ["project.directory.resolved"] }, "durable": { "type": "object", @@ -33680,3056 +21389,3589 @@ "type": "string" }, "seq": { - "type": "integer" + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "version": { - "type": "integer" + "type": "number", + "enum": [1] } }, "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { - "$ref": "#/components/schemas/LocationRef" + "$ref": "#/components/schemas/Location.Ref" }, "data": { "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "QuestionV2Request": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Info" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionV2Tool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false - }, - "QuestionV2Reply": { - "type": "object", - "properties": { - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Answer" + "properties": { + "projectID": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "previous": { + "type": "string" + } }, - "description": "User answers in order of questions (each answer is an array of selected labels)" + "required": ["projectID", "directory", "previous"], + "additionalProperties": false } }, - "required": ["answers"], + "required": ["id", "created", "type", "durable", "data"], "additionalProperties": false }, - "ReferenceLocalSource": { + "command.updated": { "type": "object", "properties": { - "type": { + "id": { "type": "string", - "enum": ["local"] + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "path": { - "type": "string" + "created": { + "type": "number" }, - "description": { - "type": "string" + "metadata": { + "type": "object" }, - "hidden": { - "type": "boolean" - } - }, - "required": ["type", "path"], - "additionalProperties": false - }, - "ReferenceGitSource": { - "type": "object", - "properties": { "type": { "type": "string", - "enum": ["git"] - }, - "repository": { - "type": "string" - }, - "branch": { - "type": "string" - }, - "description": { - "type": "string" - }, - "hidden": { - "type": "boolean" - } - }, - "required": ["type", "repository"], - "additionalProperties": false - }, - "ReferenceSource": { - "anyOf": [ - { - "$ref": "#/components/schemas/ReferenceLocalSource" - }, - { - "$ref": "#/components/schemas/ReferenceGitSource" - } - ] - }, - "ReferenceInfo": { - "type": "object", - "properties": { - "name": { - "type": "string" + "enum": ["command.updated"] }, - "path": { - "type": "string" - }, - "description": { - "type": "string" - }, - "hidden": { - "type": "boolean" + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "source": { - "$ref": "#/components/schemas/ReferenceSource" + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] } }, - "required": ["name", "path", "source"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "ProjectCopyCopy": { + "config.updated": { "type": "object", "properties": { - "directory": { - "type": "string" + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["config.updated"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] } }, - "required": ["directory"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventModels-devRefreshed": { + "skill.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["models-dev.refreshed"] + "enum": ["skill.updated"] }, - "properties": { - "type": "object", - "properties": {} + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventIntegrationUpdated": { + "Pty": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^pty" + } + ] }, - "type": { + "title": { + "type": "string" + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "status": { "type": "string", - "enum": ["integration.updated"] + "enum": ["running", "exited"] }, - "properties": { - "type": "object", - "properties": {} + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exitCode": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, - "required": ["id", "type", "properties"], + "required": ["id", "title", "command", "args", "cwd", "status", "pid"], "additionalProperties": false }, - "EventIntegrationConnectionUpdated": { + "pty.created": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["integration.connection.updated"] + "enum": ["pty.created"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "integrationID": { - "type": "string" + "info": { + "$ref": "#/components/schemas/Pty" } }, - "required": ["integrationID"], + "required": ["info"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventCatalogUpdated": { + "pty.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["catalog.updated"] + "enum": ["pty.updated"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", - "properties": {} + "properties": { + "info": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": ["info"], + "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionCreated": { + "pty.exited": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.created"] + "enum": ["pty.exited"] }, - "properties": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { "type": "object", "properties": { - "sessionID": { + "id": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^pty" + } + ] }, - "info": { - "$ref": "#/components/schemas/Session" + "exitCode": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, - "required": ["sessionID", "info"], + "required": ["id", "exitCode"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionUpdated": { + "pty.deleted": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.updated"] + "enum": ["pty.deleted"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "sessionID": { + "id": { "type": "string", - "pattern": "^ses" - }, - "info": { - "$ref": "#/components/schemas/Session" + "allOf": [ + { + "pattern": "^pty" + } + ] } }, - "required": ["sessionID", "info"], + "required": ["id"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionDeleted": { + "shell.created": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.deleted"] + "enum": ["shell.created"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/Shell.Info" } }, - "required": ["sessionID", "info"], + "required": ["info"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventMessageUpdated": { + "shell.exited": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["message.updated"] + "enum": ["shell.exited"] }, - "properties": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { "type": "object", "properties": { - "sessionID": { + "id": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^sh_" + } + ] }, - "info": { - "$ref": "#/components/schemas/Message" + "exit": { + "type": "number" + }, + "status": { + "type": "string", + "enum": ["running", "exited", "timeout", "killed"] } }, - "required": ["sessionID", "info"], + "required": ["id", "status"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventMessageRemoved": { + "shell.deleted": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["message.removed"] + "enum": ["shell.deleted"] }, - "properties": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { + "id": { "type": "string", - "pattern": "^msg" + "allOf": [ + { + "pattern": "^sh_" + } + ] } }, - "required": ["sessionID", "messageID"], + "required": ["id"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventMessagePartUpdated": { + "Question.Option": { "type": "object", "properties": { - "id": { + "label": { "type": "string", - "pattern": "^evt_" + "description": "Display text (1-5 words, concise)" }, - "type": { + "description": { "type": "string", - "enum": ["message.part.updated"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "part": { - "$ref": "#/components/schemas/Part" - }, - "time": { - "type": "number" - } - }, - "required": ["sessionID", "part", "time"], - "additionalProperties": false + "description": "Explanation of choice" } }, - "required": ["id", "type", "properties"], + "required": ["label", "description"], "additionalProperties": false }, - "EventMessagePartRemoved": { + "Question.Info": { "type": "object", "properties": { - "id": { + "question": { "type": "string", - "pattern": "^evt_" + "description": "Complete question" }, - "type": { + "header": { "type": "string", - "enum": ["message.part.removed"] + "description": "Very short label (max 30 chars)" }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - } + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Question.Option" }, - "required": ["sessionID", "messageID", "partID"], - "additionalProperties": false + "description": "Available choices" + }, + "multiple": { + "type": "boolean" + }, + "custom": { + "type": "boolean" } }, - "required": ["id", "type", "properties"], + "required": ["question", "header", "options"], "additionalProperties": false }, - "EventSessionNextAgentSwitched": { + "Question.Tool": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.agent.switched"] + "messageID": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "messageID", "agent"], - "additionalProperties": false + "id": { + "type": "string" } }, - "required": ["id", "type", "properties"], + "required": ["messageID", "id"], "additionalProperties": false }, - "EventSessionNextModelSwitched": { + "question.asked": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.model.switched"] + "enum": ["question.asked"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "messageID": { - "type": "string", - "pattern": "^msg_" + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Question.Info" + }, + "description": "Questions to ask" }, - "model": { - "$ref": "#/components/schemas/ModelRef" + "tool": { + "$ref": "#/components/schemas/Question.Tool" } }, - "required": ["timestamp", "sessionID", "messageID", "model"], + "required": ["id", "sessionID", "questions"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextMoved": { + "Question.Answer": { + "type": "array", + "items": { + "type": "string" + } + }, + "question.replied": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.moved"] + "enum": ["question.replied"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "location": { - "$ref": "#/components/schemas/LocationRef" + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] }, - "subdirectory": { - "type": "string" + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Question.Answer" + } } }, - "required": ["timestamp", "sessionID", "location"], + "required": ["sessionID", "requestID", "answers"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextPrompted": { + "question.rejected": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.prompted"] + "enum": ["question.rejected"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "delivery": { + "requestID": { "type": "string", - "enum": ["steer", "queue"] + "allOf": [ + { + "pattern": "^que" + } + ] } }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], + "required": ["sessionID", "requestID"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextPromptAdmitted": { + "Form.Metadata1": { + "type": "object" + }, + "Form.When1": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "key": { + "type": "string" }, - "type": { + "op": { "type": "string", - "enum": ["session.next.prompt.admitted"] + "enum": ["eq", "neq"] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" + "value": { + "anyOf": [ + { + "type": "string" }, - "prompt": { - "$ref": "#/components/schemas/Prompt" + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] + { + "type": "boolean" } - }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "properties"], + "required": ["key", "op", "value"], "additionalProperties": false }, - "EventSessionNextContextUpdated": { + "Form.StringField1": { "type": "object", "properties": { - "id": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { "type": "string", - "pattern": "^evt_" + "enum": ["string"] }, - "type": { + "format": { "type": "string", - "enum": ["session.next.context.updated"] + "enum": ["email", "uri", "date", "date-time"] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { - "type": "string" + "minLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false + ] + }, + "maxLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "pattern": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "default": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "custom": { + "type": "boolean" } }, - "required": ["id", "type", "properties"], + "required": ["key", "type"], "additionalProperties": false }, - "EventSessionNextSynthetic": { + "Form.NumberField1": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } }, "type": { "type": "string", - "enum": ["session.next.synthetic"] + "enum": ["number"] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { + "minimum": { + "anyOf": [ + { "type": "number" }, - "sessionID": { + { "type": "string", - "pattern": "^ses" + "enum": ["NaN"] }, - "messageID": { + { "type": "string", - "pattern": "^msg_" + "enum": ["Infinity"] }, - "text": { - "type": "string" + { + "type": "string", + "enum": ["-Infinity"] } - }, - "required": ["timestamp", "sessionID", "messageID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextShellStarted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.started"] + ] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { + "maximum": { + "anyOf": [ + { "type": "number" }, - "sessionID": { + { "type": "string", - "pattern": "^ses" + "enum": ["NaN"] }, - "messageID": { + { "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" + "enum": ["Infinity"] }, - "command": { - "type": "string" + { + "type": "string", + "enum": ["-Infinity"] } - }, - "required": ["timestamp", "sessionID", "messageID", "callID", "command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextShellEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.shell.ended"] + ] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { + "default": { + "anyOf": [ + { "type": "number" }, - "sessionID": { + { "type": "string", - "pattern": "^ses" + "enum": ["NaN"] }, - "callID": { - "type": "string" + { + "type": "string", + "enum": ["Infinity"] }, - "output": { - "type": "string" + { + "type": "string", + "enum": ["-Infinity"] } - }, - "required": ["timestamp", "sessionID", "callID", "output"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "properties"], + "required": ["key", "type"], "additionalProperties": false }, - "EventSessionNextStepStarted": { + "Form.IntegerField1": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } }, "type": { "type": "string", - "enum": ["session.next.step.started"] + "enum": ["integer"] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { + "minimum": { + "anyOf": [ + { "type": "number" }, - "sessionID": { + { "type": "string", - "pattern": "^ses" + "enum": ["NaN"] }, - "assistantMessageID": { + { "type": "string", - "pattern": "^msg_" - }, - "agent": { - "type": "string" - }, - "model": { - "$ref": "#/components/schemas/ModelRef" + "enum": ["Infinity"] }, - "snapshot": { - "type": "string" + { + "type": "string", + "enum": ["-Infinity"] } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextStepEnded": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.step.ended"] + ] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { + "maximum": { + "anyOf": [ + { "type": "number" }, - "sessionID": { + { "type": "string", - "pattern": "^ses" + "enum": ["NaN"] }, - "assistantMessageID": { + { "type": "string", - "pattern": "^msg_" - }, - "finish": { - "type": "string" + "enum": ["Infinity"] }, - "cost": { + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "default": { + "anyOf": [ + { "type": "number" }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false - }, - "snapshot": { - "type": "string" + { + "type": "string", + "enum": ["NaN"] }, - "files": { - "type": "array", - "items": { - "type": "string" - } + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "properties"], + "required": ["key", "type"], "additionalProperties": false }, - "EventSessionNextStepFailed": { + "Form.BooleanField1": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } }, "type": { "type": "string", - "enum": ["session.next.step.failed"] + "enum": ["boolean"] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "error"], - "additionalProperties": false + "default": { + "type": "boolean" } }, - "required": ["id", "type", "properties"], + "required": ["key", "type"], "additionalProperties": false }, - "EventSessionNextTextStarted": { + "Form.MultiselectField1": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } }, "type": { "type": "string", - "enum": ["session.next.text.started"] + "enum": ["multiselect"] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "minItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], - "additionalProperties": false + ] + }, + "maxItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "custom": { + "type": "boolean" + }, + "default": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": ["id", "type", "properties"], + "required": ["key", "type", "options"], "additionalProperties": false }, - "EventSessionNextTextDelta": { + "Form.Field1": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField1" + }, + { + "$ref": "#/components/schemas/Form.NumberField1" + }, + { + "$ref": "#/components/schemas/Form.IntegerField1" + }, + { + "$ref": "#/components/schemas/Form.BooleanField1" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField1" + }, + { + "$ref": "#/components/schemas/Form.ExternalField" + } + ] + }, + "Form.Fields3": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field1" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field1" + } + }, + "Form.Info1": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^frm_" + } + ] }, - "type": { - "type": "string", - "enum": ["session.next.text.delta"] + "sessionID": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], - "additionalProperties": false + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata1" + }, + "fields": { + "$ref": "#/components/schemas/Form.Fields3" } }, - "required": ["id", "type", "properties"], + "required": ["id", "sessionID", "title", "fields"], "additionalProperties": false }, - "EventSessionNextTextEnded": { + "form.created": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.text.ended"] + "enum": ["form.created"] }, - "properties": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { "type": "object", "properties": { - "timestamp": { + "form": { + "$ref": "#/components/schemas/Form.Info1" + } + }, + "required": ["form"], + "additionalProperties": false + } + }, + "required": ["id", "created", "type", "data"], + "additionalProperties": false + }, + "Form.Value1": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { "type": "number" }, - "sessionID": { + { "type": "string", - "pattern": "^ses" + "enum": ["NaN"] }, - "assistantMessageID": { + { "type": "string", - "pattern": "^msg_" - }, - "textID": { - "type": "string" + "enum": ["Infinity"] }, - "text": { - "type": "string" + { + "type": "string", + "enum": ["-Infinity"] } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], - "additionalProperties": false + ] + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false + ] + }, + "Form.Answer1": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Form.Value1" + } }, - "EventSessionNextReasoningStarted": { + "form.replied": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.reasoning.started"] + "enum": ["form.replied"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { + "id": { "type": "string", - "pattern": "^msg_" + "allOf": [ + { + "pattern": "^frm_" + } + ] }, - "reasoningID": { + "sessionID": { "type": "string" }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + "answer": { + "$ref": "#/components/schemas/Form.Answer1" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], + "required": ["id", "sessionID", "answer"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextReasoningDelta": { + "form.cancelled": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.reasoning.delta"] + "enum": ["form.cancelled"] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { "type": "string", - "pattern": "^msg_" - }, - "reasoningID": { - "type": "string" + "allOf": [ + { + "pattern": "^frm_" + } + ] }, - "delta": { + "sessionID": { "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], + "required": ["id", "sessionID"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextReasoningEnded": { + "websearch.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.reasoning.ended"] + "enum": ["websearch.updated"] }, - "properties": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": ["id", "created", "type", "data"], + "additionalProperties": false + }, + "SessionStatus": { + "anyOf": [ + { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { + "type": { "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { + "enum": ["idle"] + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { "type": "string", - "pattern": "^msg_" + "enum": ["retry"] }, - "reasoningID": { - "type": "string" + "attempt": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "text": { + "message": { "type": "string" }, - "providerMetadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + "action": { + "type": "object", + "properties": { + "reason": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "label": { + "type": "string" + }, + "link": { + "type": "string" + } + }, + "required": ["reason", "provider", "title", "message", "label"], + "additionalProperties": false + }, + "next": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["type", "attempt", "message", "next"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["busy"] } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], + "required": ["type"], "additionalProperties": false } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false + ] }, - "EventSessionNextToolInputStarted": { + "session.status": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.input.started"] + "enum": ["session.status"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "name": { - "type": "string" + "status": { + "$ref": "#/components/schemas/SessionStatus" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], + "required": ["sessionID", "status"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextToolInputDelta": { + "session.idle": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.input.delta"] + "enum": ["session.idle"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "delta": { - "type": "string" + "allOf": [ + { + "pattern": "^ses" + } + ] } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], + "required": ["sessionID"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextToolInputEnded": { + "tui.prompt.append": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.input.ended"] + "enum": ["tui.prompt.append"] }, - "properties": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, "text": { "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], + "required": ["text"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextToolCalled": { + "tui.command.execute": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.called"] + "enum": ["tui.command.execute"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "input": { - "type": "object" - }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "session.background", + "session.compact", + "session.page.up", + "session.page.down", + "session.line.up", + "session.line.down", + "session.half.page.up", + "session.half.page.down", + "session.first", + "session.last", + "prompt.clear", + "prompt.submit", + "agent.cycle" + ] }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + { + "type": "string" } - }, - "required": ["executed"], - "additionalProperties": false + ] } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], + "required": ["command"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextToolProgress": { + "tui.toast.show": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] }, - "type": { - "type": "string", - "enum": ["session.next.tool.progress"] + "created": { + "type": "number" }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionNextToolSuccess": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.success"] + "enum": ["tui.toast.show"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { + "title": { "type": "string" }, - "structured": { - "type": "object" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LLMToolContent" - } + "message": { + "type": "string" }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } + "variant": { + "type": "string", + "enum": ["info", "success", "warning", "error"] }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" + "duration": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + { + "type": "null" } - }, - "required": ["executed"], - "additionalProperties": false + ] } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], + "required": ["message", "variant"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextToolFailed": { + "tui.session.select": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.failed"] + "enum": ["tui.session.select"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", - "pattern": "^ses" - }, - "assistantMessageID": { - "type": "string", - "pattern": "^msg_" - }, - "callID": { - "type": "string" - }, - "error": { - "$ref": "#/components/schemas/SessionErrorUnknown" - }, - "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLMProviderMetadata" + "allOf": [ + { + "pattern": "^ses", + "description": "Session ID to navigate to" } - }, - "required": ["executed"], - "additionalProperties": false + ] } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], + "required": ["sessionID"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextRetried": { + "installation.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.retried"] + "enum": ["installation.updated"] }, - "properties": { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "attempt": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/SessionNextRetry_error" + "version": { + "type": "string" } }, - "required": ["timestamp", "sessionID", "attempt", "error"], + "required": ["version"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextCompactionStarted": { + "installation.update-available": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.compaction.started"] + "enum": ["installation.update-available"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] + "version": { + "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "reason"], + "required": ["version"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextCompactionDelta": { + "vcs.branch.updated": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.compaction.delta"] + "enum": ["vcs.branch.updated"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "text": { + "branch": { "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "text"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextCompactionEnded": { + "mcp.status.changed": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.compaction.ended"] + "enum": ["mcp.status.changed"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - }, - "text": { - "type": "string" - }, - "recent": { + "server": { "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], + "required": ["server"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextRevertStaged": { + "mcp.resources.changed": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" }, "type": { "type": "string", - "enum": ["session.next.revert.staged"] + "enum": ["mcp.resources.changed"] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" }, - "properties": { + "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "revert": { - "$ref": "#/components/schemas/RevertState" + "server": { + "type": "string" } }, - "required": ["timestamp", "sessionID", "revert"], + "required": ["server"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "created", "type", "data"], "additionalProperties": false }, - "EventSessionNextRevertCleared": { + "V2Event.server.connected": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/Location.Ref" + }, + { + "type": "null" + } + ] }, "type": { "type": "string", - "enum": ["session.next.revert.cleared"] + "enum": ["server.connected"] }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" + "data": { + "anyOf": [ + { + "type": "object" }, - "sessionID": { - "type": "string", - "pattern": "^ses" + { + "type": "array" } - }, - "required": ["timestamp", "sessionID"], - "additionalProperties": false + ] + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "V2Event": { + "anyOf": [ + { + "$ref": "#/components/schemas/models-dev.refreshed" + }, + { + "$ref": "#/components/schemas/integration.updated" + }, + { + "$ref": "#/components/schemas/integration.connection.updated" + }, + { + "$ref": "#/components/schemas/catalog.updated" + }, + { + "$ref": "#/components/schemas/agent.updated" + }, + { + "$ref": "#/components/schemas/session.created" + }, + { + "$ref": "#/components/schemas/session.agent.selected" + }, + { + "$ref": "#/components/schemas/session.model.selected" + }, + { + "$ref": "#/components/schemas/session.moved" + }, + { + "$ref": "#/components/schemas/session.renamed" + }, + { + "$ref": "#/components/schemas/session.usage.updated" + }, + { + "$ref": "#/components/schemas/session.deleted" + }, + { + "$ref": "#/components/schemas/session.forked" + }, + { + "$ref": "#/components/schemas/session.inbox.delivered" + }, + { + "$ref": "#/components/schemas/session.inbox.enqueued" + }, + { + "$ref": "#/components/schemas/session.inbox.cancelled" + }, + { + "$ref": "#/components/schemas/session.inbox.delivery.changed" + }, + { + "$ref": "#/components/schemas/session.execution.started" + }, + { + "$ref": "#/components/schemas/session.execution.succeeded" + }, + { + "$ref": "#/components/schemas/session.execution.failed" + }, + { + "$ref": "#/components/schemas/session.execution.interrupted" + }, + { + "$ref": "#/components/schemas/session.instructions.updated" + }, + { + "$ref": "#/components/schemas/session.synthetic" + }, + { + "$ref": "#/components/schemas/session.skill.activated" + }, + { + "$ref": "#/components/schemas/session.shell.started" + }, + { + "$ref": "#/components/schemas/session.shell.ended" + }, + { + "$ref": "#/components/schemas/session.step.started" + }, + { + "$ref": "#/components/schemas/session.step.ended" + }, + { + "$ref": "#/components/schemas/session.step.failed" + }, + { + "$ref": "#/components/schemas/session.text.started" + }, + { + "$ref": "#/components/schemas/session.text.delta" + }, + { + "$ref": "#/components/schemas/session.text.ended" + }, + { + "$ref": "#/components/schemas/session.reasoning.started" + }, + { + "$ref": "#/components/schemas/session.reasoning.delta" + }, + { + "$ref": "#/components/schemas/session.reasoning.ended" + }, + { + "$ref": "#/components/schemas/session.tool.input.started" + }, + { + "$ref": "#/components/schemas/session.tool.input.delta" + }, + { + "$ref": "#/components/schemas/session.tool.input.ended" + }, + { + "$ref": "#/components/schemas/session.tool.called" + }, + { + "$ref": "#/components/schemas/session.tool.progress" + }, + { + "$ref": "#/components/schemas/session.tool.success" + }, + { + "$ref": "#/components/schemas/session.tool.failed" + }, + { + "$ref": "#/components/schemas/session.retry.scheduled" + }, + { + "$ref": "#/components/schemas/session.compaction.started" + }, + { + "$ref": "#/components/schemas/session.compaction.delta" + }, + { + "$ref": "#/components/schemas/session.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.compaction.failed" + }, + { + "$ref": "#/components/schemas/session.revert.staged" + }, + { + "$ref": "#/components/schemas/session.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.revert.committed" + }, + { + "$ref": "#/components/schemas/filesystem.changed" + }, + { + "$ref": "#/components/schemas/reference.updated" + }, + { + "$ref": "#/components/schemas/permission.asked" + }, + { + "$ref": "#/components/schemas/permission.replied" + }, + { + "$ref": "#/components/schemas/plugin.added" + }, + { + "$ref": "#/components/schemas/plugin.updated" + }, + { + "$ref": "#/components/schemas/project.directories.updated" + }, + { + "$ref": "#/components/schemas/project.directory.resolved" + }, + { + "$ref": "#/components/schemas/command.updated" + }, + { + "$ref": "#/components/schemas/config.updated" + }, + { + "$ref": "#/components/schemas/skill.updated" + }, + { + "$ref": "#/components/schemas/pty.created" + }, + { + "$ref": "#/components/schemas/pty.updated" + }, + { + "$ref": "#/components/schemas/pty.exited" + }, + { + "$ref": "#/components/schemas/pty.deleted" + }, + { + "$ref": "#/components/schemas/shell.created" + }, + { + "$ref": "#/components/schemas/shell.exited" + }, + { + "$ref": "#/components/schemas/shell.deleted" + }, + { + "$ref": "#/components/schemas/question.asked" + }, + { + "$ref": "#/components/schemas/question.replied" + }, + { + "$ref": "#/components/schemas/question.rejected" + }, + { + "$ref": "#/components/schemas/form.created" + }, + { + "$ref": "#/components/schemas/form.replied" + }, + { + "$ref": "#/components/schemas/form.cancelled" + }, + { + "$ref": "#/components/schemas/websearch.updated" + }, + { + "$ref": "#/components/schemas/session.status" + }, + { + "$ref": "#/components/schemas/session.idle" + }, + { + "$ref": "#/components/schemas/tui.prompt.append" + }, + { + "$ref": "#/components/schemas/tui.command.execute" + }, + { + "$ref": "#/components/schemas/tui.toast.show" + }, + { + "$ref": "#/components/schemas/tui.session.select" + }, + { + "$ref": "#/components/schemas/installation.updated" + }, + { + "$ref": "#/components/schemas/installation.update-available" + }, + { + "$ref": "#/components/schemas/vcs.branch.updated" + }, + { + "$ref": "#/components/schemas/mcp.status.changed" + }, + { + "$ref": "#/components/schemas/mcp.resources.changed" + }, + { + "$ref": "#/components/schemas/V2Event.server.connected" + } + ] + }, + "V2EventJsonString": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/V2Event" + }, + "contentMediaType": "application/json" + }, + "PtyNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["PtyNotFoundError"] + }, + "ptyID": { + "type": "string" + }, + "message": { + "type": "string" } }, - "required": ["id", "type", "properties"], + "required": ["_tag", "ptyID", "message"], "additionalProperties": false }, - "EventSessionNextRevertCommitted": { + "PtyTicket.ConnectToken": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.next.revert.committed"] + "ticket": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg_" + "expires_in": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 } - }, - "required": ["timestamp", "sessionID", "messageID"], - "additionalProperties": false + ] } }, - "required": ["id", "type", "properties"], + "required": ["ticket", "expires_in"], "additionalProperties": false }, - "EventMessagePartDelta": { + "ForbiddenError": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { + "_tag": { "type": "string", - "enum": ["message.part.delta"] + "enum": ["ForbiddenError"] }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "messageID": { - "type": "string", - "pattern": "^msg" - }, - "partID": { - "type": "string", - "pattern": "^prt" - }, - "field": { - "type": "string" - }, - "delta": { - "type": "string" - } - }, - "required": ["sessionID", "messageID", "partID", "field", "delta"], - "additionalProperties": false + "message": { + "type": "string" } }, - "required": ["id", "type", "properties"], + "required": ["_tag", "message"], "additionalProperties": false }, - "EventSessionDiff": { + "Shell.Info1": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^sh_" + } + ] }, - "type": { + "status": { "type": "string", - "enum": ["session.diff"] + "enum": ["running", "exited", "timeout", "killed"] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exit": { + "type": "number" + }, + "metadata": { + "type": "object" }, - "properties": { + "time": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" + "started": { + "type": "number" }, - "diff": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" - } + "completed": { + "type": "number" } }, - "required": ["sessionID", "diff"], + "required": ["started"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["id", "status", "command", "cwd", "shell", "file", "metadata", "time"], "additionalProperties": false }, - "EventSessionError": { + "ShellNotFoundError": { "type": "object", "properties": { - "id": { + "_tag": { "type": "string", - "pattern": "^evt_" + "enum": ["ShellNotFoundError"] }, - "type": { - "type": "string", - "enum": ["session.error"] + "id": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/StructuredOutputError" - }, - { - "$ref": "#/components/schemas/ContextOverflowError" - }, - { - "$ref": "#/components/schemas/ContentFilterError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - } - }, - "additionalProperties": false + "message": { + "type": "string" } }, - "required": ["id", "type", "properties"], + "required": ["_tag", "id", "message"], "additionalProperties": false }, - "EventInstallationUpdated": { + "Question.Request": { "type": "object", "properties": { "id": { "type": "string", - "pattern": "^evt_" + "allOf": [ + { + "pattern": "^que" + } + ] }, - "type": { + "sessionID": { "type": "string", - "enum": ["installation.updated"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" + "allOf": [ + { + "pattern": "^ses" } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Question.Info" }, - "required": ["version"], - "additionalProperties": false + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/Question.Tool" } }, - "required": ["id", "type", "properties"], + "required": ["id", "sessionID", "questions"], "additionalProperties": false }, - "EventInstallationUpdate-available": { + "Question.Reply": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["installation.update-available"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Question.Answer" }, - "required": ["version"], - "additionalProperties": false + "description": "User answers in order of questions (each answer is an array of selected labels)" } }, - "required": ["id", "type", "properties"], + "required": ["answers"], "additionalProperties": false }, - "EventFileEdited": { + "QuestionNotFoundError": { "type": "object", "properties": { - "id": { + "_tag": { "type": "string", - "pattern": "^evt_" + "enum": ["QuestionNotFoundError"] }, - "type": { - "type": "string", - "enum": ["file.edited"] + "requestID": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"], - "additionalProperties": false + "message": { + "type": "string" } }, - "required": ["id", "type", "properties"], + "required": ["_tag", "requestID", "message"], "additionalProperties": false }, - "EventReferenceUpdated": { + "Reference.LocalSource": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, "type": { "type": "string", - "enum": ["reference.updated"] + "enum": ["local"] }, - "properties": { - "type": "object", - "properties": {} + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" } }, - "required": ["id", "type", "properties"], + "required": ["type", "path"], "additionalProperties": false }, - "EventPermissionV2Asked": { + "Reference.GitSource": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, "type": { "type": "string", - "enum": ["permission.v2.asked"] + "enum": ["git"] }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "action": { - "type": "string" - }, - "resources": { - "type": "array", - "items": { - "type": "string" - } - }, - "save": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "source": { - "$ref": "#/components/schemas/PermissionV2Source" - } - }, - "required": ["id", "sessionID", "action", "resources"], - "additionalProperties": false + "repository": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" } }, - "required": ["id", "type", "properties"], + "required": ["type", "repository"], "additionalProperties": false }, - "EventPermissionV2Replied": { + "Reference.Source": { + "anyOf": [ + { + "$ref": "#/components/schemas/Reference.LocalSource" + }, + { + "$ref": "#/components/schemas/Reference.GitSource" + } + ] + }, + "Reference.Info": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "name": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["permission.v2.replied"] + "path": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "source": { + "$ref": "#/components/schemas/Reference.Source" } }, - "required": ["id", "type", "properties"], + "required": ["name", "path", "source"], "additionalProperties": false }, - "EventPluginAdded": { + "ProjectCopy.Copy": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["plugin.added"] - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": ["id"], - "additionalProperties": false + "directory": { + "type": "string" } }, - "required": ["id", "type", "properties"], + "required": ["directory"], "additionalProperties": false }, - "EventProjectDirectoriesUpdated": { + "ProjectCopyError": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { + "name": { "type": "string", - "enum": ["project.directories.updated"] + "enum": ["ProjectCopyError"] }, - "properties": { + "data": { "type": "object", "properties": { - "projectID": { + "message": { "type": "string" + }, + "forceRequired": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] } }, - "required": ["projectID"], + "required": ["message"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["name", "data"], "additionalProperties": false }, - "EventFileWatcherUpdated": { + "Vcs.Branch": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["file.watcher.updated"] + "current": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "type": "string", - "enum": ["add", "change", "unlink"] - } - }, - "required": ["file", "event"], - "additionalProperties": false + "default": { + "type": "string" } }, - "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventPtyCreated": { + "Vcs.Info": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.created"] - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": ["info"], - "additionalProperties": false + "branch": { + "$ref": "#/components/schemas/Vcs.Branch" } }, - "required": ["id", "type", "properties"], + "required": ["branch"], "additionalProperties": false }, - "EventPtyUpdated": { + "Vcs.FileStatus": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "file": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["pty.updated"] + "additions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" + "deletions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 } - }, - "required": ["info"], - "additionalProperties": false + ] + }, + "status": { + "type": "string", + "enum": ["added", "deleted", "modified"] } }, - "required": ["id", "type", "properties"], + "required": ["file", "additions", "deletions", "status"], "additionalProperties": false }, - "EventPtyExited": { + "Vcs.Mode": { + "type": "string", + "enum": ["working", "branch"] + }, + "WebSearch.Provider": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["pty.exited"] + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty" - }, - "exitCode": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["id", "exitCode"], - "additionalProperties": false + "name": { + "type": "string" } }, - "required": ["id", "type", "properties"], + "required": ["id", "name"], "additionalProperties": false }, - "EventPtyDeleted": { + "WebSearch.Result": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "url": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["pty.deleted"] + "title": { + "type": "string" + }, + "content": { + "type": "string" }, - "properties": { + "time": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^pty" + "published": { + "type": "number" } }, - "required": ["id"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["url", "time"], "additionalProperties": false }, - "EventQuestionV2Asked": { + "WebSearch.Response": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.asked"] + "providerID": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Info" - }, - "description": "Questions to ask" - }, - "tool": { - "$ref": "#/components/schemas/QuestionV2Tool" - } - }, - "required": ["id", "sessionID", "questions"], - "additionalProperties": false + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebSearch.Result" + } } }, - "required": ["id", "type", "properties"], + "required": ["providerID", "results"], "additionalProperties": false }, - "EventQuestionV2Replied": { + "Config.Agent": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["question.v2.replied"] + "model": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+\\/[^#]+(?:#[^#]+)?$" + } + ] + }, + { + "type": "object", + "properties": { + "providerID": { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+$" + } + ] + }, + "model": { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + }, + "variant": { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + } + }, + "required": ["providerID", "model"], + "additionalProperties": false + } + ] }, - "properties": { + "request": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionV2Answer" + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" } + }, + "body": { + "type": "object" } }, - "required": ["sessionID", "requestID", "answers"], "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventQuestionV2Rejected": { - "type": "object", - "properties": { - "id": { + }, + "system": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mode": { "type": "string", - "pattern": "^evt_" + "enum": ["subagent", "primary", "all"] }, - "type": { + "hidden": { + "type": "boolean" + }, + "color": { "type": "string", - "enum": ["question.v2.rejected"] + "allOf": [ + { + "pattern": "^#[0-9a-fA-F]{6}$" + } + ] }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" + "steps": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 } - }, - "required": ["sessionID", "requestID"], - "additionalProperties": false + ] + }, + "disabled": { + "type": "boolean" + }, + "permissions": { + "$ref": "#/components/schemas/Permission.Ruleset" } }, - "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventTodoUpdated": { + "Config.Formatter.Entry": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "disabled": { + "type": "boolean" }, - "type": { - "type": "string", - "enum": ["todo.updated"] + "command": { + "type": "array", + "items": { + "type": "string" + } }, - "properties": { + "environment": { "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": ["sessionID", "todos"], - "additionalProperties": false + "additionalProperties": { + "type": "string" + } + }, + "extensions": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventLspUpdated": { + "Config.LSP.Server": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "command": { + "type": "array", + "items": { + "type": "string" + } }, - "type": { - "type": "string", - "enum": ["lsp.updated"] + "extensions": { + "type": "array", + "items": { + "type": "string" + } + }, + "disabled": { + "type": "boolean" }, - "properties": { + "env": { "type": "object", - "properties": {} + "additionalProperties": { + "type": "string" + } + }, + "initialization": { + "type": "object" } }, - "required": ["id", "type", "properties"], + "required": ["command"], "additionalProperties": false }, - "EventPermissionAsked": { + "Config.Command": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "template": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["permission.asked"] + "description": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per" - }, - "sessionID": { + "agent": { + "type": "string" + }, + "model": { + "anyOf": [ + { "type": "string", - "pattern": "^ses" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } + "allOf": [ + { + "pattern": "^[^/#]+\\/[^#]+(?:#[^#]+)?$" + } + ] }, - "tool": { + { "type": "object", "properties": { - "messageID": { - "type": "string" + "providerID": { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+$" + } + ] }, - "callID": { - "type": "string" + "model": { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + }, + "variant": { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] } }, - "required": ["messageID", "callID"], + "required": ["providerID", "model"], "additionalProperties": false } - }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], - "additionalProperties": false + ] + }, + "subtask": { + "type": "boolean" } }, - "required": ["id", "type", "properties"], + "required": ["template"], "additionalProperties": false }, - "EventPermissionReplied": { + "Config.Reference.Git": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "repository": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["permission.replied"] + "branch": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^per" - }, - "reply": { - "type": "string", - "enum": ["once", "always", "reject"] - } - }, - "required": ["sessionID", "requestID", "reply"], - "additionalProperties": false + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" } }, - "required": ["id", "type", "properties"], + "required": ["repository"], "additionalProperties": false }, - "EventMcpToolsChanged": { + "Config.Reference.Local": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "path": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["mcp.tools.changed"] + "description": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "server": { - "type": "string" - } - }, - "required": ["server"], - "additionalProperties": false + "hidden": { + "type": "boolean" + } + }, + "required": ["path"], + "additionalProperties": false + }, + "Config.Plugin.Entry": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "options": { + "type": "object" } }, - "required": ["id", "type", "properties"], + "required": ["package"], "additionalProperties": false }, - "EventMcpBrowserOpenFailed": { + "Config.Warming": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "prompt": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["mcp.browser.open.failed"] + "interval": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "mcpName": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": ["mcpName", "url"], - "additionalProperties": false + "duration": { + "type": "string" } }, - "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventCommandExecuted": { + "Config.Model.Cost": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["command.executed"] - }, - "properties": { + "tier": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "sessionID": { + "type": { "type": "string", - "pattern": "^ses" + "enum": ["context"] }, - "arguments": { - "type": "string" + "size": { + "type": "integer" + } + }, + "required": ["type", "size"], + "additionalProperties": false + }, + "input": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "output": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, - "messageID": { - "type": "string", - "pattern": "^msg" + "write": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" } }, - "required": ["name", "sessionID", "arguments", "messageID"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], + "required": ["input", "output"], "additionalProperties": false }, - "EventProjectUpdated": { + "Config.Model": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "modelID": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["project.updated"] + "family": { + "type": "string" + }, + "name": { + "type": "string" + }, + "compatibility": { + "$ref": "#/components/schemas/Model.Compatibility" + }, + "package": { + "type": "string" + }, + "settings": { + "type": "object" }, - "properties": { + "headers": { "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "$ref": "#/components/schemas/ProjectVcs" - }, - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/ProjectIcon" - }, - "commands": { - "$ref": "#/components/schemas/ProjectCommands" + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + }, + "capabilities": { + "$ref": "#/components/schemas/Model.Capabilities" + }, + "variants": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } }, - "time": { - "$ref": "#/components/schemas/ProjectTime" + "required": ["id"], + "additionalProperties": false + } + }, + "cost": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Cost" }, - "sandboxes": { + { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/Config.Model.Cost" } } - }, - "required": ["id", "worktree", "time", "sandboxes"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + ] }, - "type": { - "type": "string", - "enum": ["session.status"] + "disabled": { + "type": "boolean" }, - "properties": { + "limit": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" + "context": { + "type": "integer" }, - "status": { - "$ref": "#/components/schemas/SessionStatus" + "input": { + "type": "integer" + }, + "output": { + "type": "integer" } }, - "required": ["sessionID", "status"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventSessionIdle": { + "Config.Provider": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "name": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["session.idle"] + "env": { + "type": "array", + "items": { + "type": "string" + } + }, + "package": { + "type": "string" + }, + "settings": { + "type": "object" }, - "properties": { + "headers": { "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + }, + "models": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Model" + } } }, - "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventQuestionAsked": { + "Config.Info": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "$schema": { + "type": "string" }, - "type": { - "type": "string", - "enum": ["question.asked"] + "shell": { + "type": "string" }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que" - }, - "sessionID": { + "model": { + "anyOf": [ + { "type": "string", - "pattern": "^ses" + "allOf": [ + { + "pattern": "^[^/#]+\\/[^#]+(?:#[^#]+)?$" + } + ] }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" + { + "type": "object", + "properties": { + "providerID": { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+$" + } + ] + }, + "model": { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + }, + "variant": { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + } }, - "description": "Questions to ask" + "required": ["providerID", "model"], + "additionalProperties": false + } + ] + }, + "default_agent": { + "type": "string" + }, + "autoupdate": { + "anyOf": [ + { + "type": "boolean" }, - "tool": { - "$ref": "#/components/schemas/QuestionTool" + { + "type": "string", + "enum": ["notify"] + } + ] + }, + "share": { + "type": "string", + "enum": ["manual", "auto", "disabled"] + }, + "enterprise": { + "type": "object", + "properties": { + "url": { + "type": "string" } }, - "required": ["id", "sessionID", "questions"], "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventQuestionReplied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" }, - "type": { - "type": "string", - "enum": ["question.replied"] + "username": { + "type": "string" + }, + "permissions": { + "$ref": "#/components/schemas/Permission.Ruleset" }, - "properties": { + "agents": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Agent" + } + }, + "snapshots": { + "type": "boolean" + }, + "watcher": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" - }, - "answers": { + "ignore": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionAnswer" + "type": "string" } } }, - "required": ["sessionID", "requestID", "answers"], "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventQuestionRejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" }, - "type": { - "type": "string", - "enum": ["question.rejected"] + "formatter": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Formatter.Entry" + } + } + ] + }, + "lsp": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "enum": [true] + } + }, + "required": ["disabled"], + "additionalProperties": false + }, + { + "$ref": "#/components/schemas/Config.LSP.Server" + } + ] + } + } + ] }, - "properties": { + "media": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "requestID": { - "type": "string", - "pattern": "^que" + "image": { + "type": "object", + "properties": { + "auto_resize": { + "type": "boolean" + }, + "max_width": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "max_height": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "max_base64_bytes": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "additionalProperties": false } }, - "required": ["sessionID", "requestID"], "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionCompacted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["session.compacted"] }, - "properties": { + "tool_output": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" + "max_lines": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "max_bytes": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] } }, - "required": ["sessionID"], "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventVcsBranchUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["vcs.branch.updated"] }, - "properties": { + "mcp": { "type": "object", "properties": { - "branch": { - "type": "string" + "timeout": { + "type": "object", + "properties": { + "startup": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "catalog": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "execution": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "additionalProperties": false + }, + "servers": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.LocalConfig" + }, + { + "$ref": "#/components/schemas/Mcp.RemoteConfig" + } + ] + } } }, "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorkspaceReady": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["workspace.ready"] }, - "properties": { + "compaction": { "type": "object", "properties": { - "name": { - "type": "string" + "auto": { + "type": "boolean" + }, + "keep": { + "type": "object", + "properties": { + "tokens": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "additionalProperties": false + }, + "buffer": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, - "required": ["name"], "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorkspaceFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" }, - "type": { - "type": "string", - "enum": ["workspace.failed"] + "skills": { + "type": "array", + "items": { + "type": "string" + } + }, + "commands": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Command" + } + }, + "instructions": { + "type": "array", + "items": { + "type": "string" + } + }, + "references": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Config.Reference.Git" + }, + { + "$ref": "#/components/schemas/Config.Reference.Local" + } + ] + } }, - "properties": { + "websearch": { "type": "object", "properties": { - "message": { + "provider": { "type": "string" } }, - "required": ["message"], + "required": ["provider"], "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorkspaceStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" }, - "type": { - "type": "string", - "enum": ["workspace.status"] + "plugins": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Config.Plugin.Entry" + } + ] + } }, - "properties": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk" + "warming": { + "anyOf": [ + { + "type": "boolean" }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] + { + "$ref": "#/components/schemas/Config.Warming" } - }, - "required": ["workspaceID", "status"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorktreeReady": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["worktree.ready"] + ] }, - "properties": { + "providers": { "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - } - }, - "required": ["name"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventWorktreeFailed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, - "type": { - "type": "string", - "enum": ["worktree.failed"] + "additionalProperties": { + "$ref": "#/components/schemas/Config.Provider" + } }, - "properties": { + "experimental": { "type": "object", "properties": { - "message": { - "type": "string" + "subagent_depth": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "policies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["provider.use"] + }, + "resource": { + "type": "string" + }, + "effect": { + "type": "string", + "enum": ["allow", "deny"] + } + }, + "required": ["action", "resource", "effect"], + "additionalProperties": false + } } }, - "required": ["message"], "additionalProperties": false } }, - "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventServerConnected": { + "Config.Document": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^evt_" - }, "type": { "type": "string", - "enum": ["server.connected"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventGlobalDisposed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^evt_" + "enum": ["document"] }, - "type": { - "type": "string", - "enum": ["global.disposed"] + "path": { + "type": "string" }, - "properties": { - "type": "object", - "properties": {} + "info": { + "$ref": "#/components/schemas/Config.Info" } }, - "required": ["id", "type", "properties"], + "required": ["type", "info"], "additionalProperties": false }, - "CredentialOAuth": { + "Config.Directory": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["oauth"] - }, - "methodID": { - "type": "string" - }, - "refresh": { - "type": "string" + "enum": ["directory"] }, - "access": { + "path": { "type": "string" - }, - "expires": { - "type": "integer", - "minimum": 0 - }, - "metadata": { - "type": "object" } }, - "required": ["type", "methodID", "refresh", "access", "expires"], + "required": ["type", "path"], "additionalProperties": false }, - "CredentialKey": { + "Config.File": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["key"] + "enum": ["file"] }, - "key": { + "path": { "type": "string" - }, - "metadata": { - "type": "object" } }, - "required": ["type", "key"], + "required": ["type", "path"], "additionalProperties": false }, - "SkillV2DirectorySource": { + "Config.AgentsDirectory": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["directory"] + "enum": ["agents"] }, "path": { "type": "string" @@ -36738,175 +24980,101 @@ "required": ["type", "path"], "additionalProperties": false }, - "SkillV2UrlSource": { + "Config.ClaudeDirectory": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["url"] + "enum": ["claude"] }, - "url": { + "path": { "type": "string" } }, - "required": ["type", "url"], + "required": ["type", "path"], "additionalProperties": false }, - "SkillV2EmbeddedSource": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["embedded"] + "Config.Entry": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Document" }, - "skill": { - "$ref": "#/components/schemas/SkillV2Info" - } - }, - "required": ["type", "skill"], - "additionalProperties": false - }, - "BadRequestError": { - "type": "object", - "required": ["name", "data"], - "properties": { - "name": { - "type": "string", - "enum": ["BadRequest"] + { + "$ref": "#/components/schemas/Config.Directory" }, - "data": { - "type": "object", - "required": ["message"], - "properties": { - "message": { - "type": "string" - }, - "kind": { - "type": "string", - "enum": ["Params", "Headers", "Query", "Body", "Payload"] - } - } + { + "$ref": "#/components/schemas/Config.File" + }, + { + "$ref": "#/components/schemas/Config.AgentsDirectory" + }, + { + "$ref": "#/components/schemas/Config.ClaudeDirectory" } - } + ] } - } + }, + "securitySchemes": {} }, "security": [], "tags": [ { - "name": "control", - "description": "Control plane routes." + "name": "health" }, { - "name": "controlPlane", - "description": "Control-plane orchestration routes." + "name": "server" }, { - "name": "global", - "description": "Global server routes." + "name": "location" }, { - "name": "event", - "description": "Instance event stream route." - }, - { - "name": "config", - "description": "Experimental HttpApi config routes." - }, - { - "name": "experimental", - "description": "Experimental HttpApi read-only routes." - }, - { - "name": "file", - "description": "Experimental HttpApi file routes." - }, - { - "name": "instance", - "description": "Experimental HttpApi instance read routes." - }, - { - "name": "mcp", - "description": "Experimental HttpApi MCP routes." - }, - { - "name": "project", - "description": "Experimental HttpApi project routes." + "name": "agent" }, { - "name": "projectCopy", - "description": "Project copy naming routes." - }, - { - "name": "pty", - "description": "Experimental HttpApi PTY routes." - }, - { - "name": "question", - "description": "Question routes." - }, - { - "name": "permission", - "description": "Experimental HttpApi permission routes." - }, - { - "name": "provider", - "description": "Experimental HttpApi provider routes." + "name": "plugin", + "description": "Experimental plugin routes." }, { "name": "session", - "description": "Experimental HttpApi session routes." - }, - { - "name": "sync", - "description": "Experimental HttpApi sync routes." - }, - { - "name": "tui", - "description": "Experimental HttpApi TUI routes." - }, - { - "name": "workspace", - "description": "Experimental HttpApi workspace routes." + "description": "Experimental session routes." }, { - "name": "opencode HttpApi", - "description": "Experimental HttpApi surface for selected instance routes." + "name": "session", + "description": "Experimental message routes." }, { - "name": "opencode HttpApi", - "description": "Experimental HttpApi surface for selected instance routes." + "name": "model", + "description": "Experimental model routes." }, { - "name": "opencode HttpApi", - "description": "Experimental HttpApi surface for selected instance routes." + "name": "generate", + "description": "Experimental one-shot generation routes." }, { - "name": "sessions", - "description": "Experimental session routes." + "name": "provider", + "description": "Experimental provider routes." }, { - "name": "messages", - "description": "Experimental message routes." + "name": "integration", + "description": "Integration discovery and authentication routes." }, { - "name": "models", - "description": "Experimental model routes." + "name": "mcp", + "description": "MCP server and resource routes." }, { - "name": "providers", - "description": "Experimental provider routes." + "name": "credential" }, { - "name": "integrations", - "description": "Integration discovery and authentication routes." + "name": "project", + "description": "Location-scoped project routes." }, { - "name": "opencode HttpApi", - "description": "Experimental HttpApi surface for selected instance routes." + "name": "form", + "description": "Session form routes." }, { - "name": "permissions", + "name": "permission", "description": "Experimental permission routes." }, { @@ -36914,23 +25082,27 @@ "description": "Experimental location-scoped filesystem routes." }, { - "name": "commands", + "name": "command", "description": "Experimental command routes." }, { - "name": "skills", + "name": "skill", "description": "Experimental skill routes." }, { - "name": "events", - "description": "Experimental event stream route." + "name": "event", + "description": "Experimental event stream routes." }, { "name": "pty", "description": "Experimental location-scoped PTY routes." }, { - "name": "session questions", + "name": "shell", + "description": "Experimental location-scoped shell command routes." + }, + { + "name": "question", "description": "Experimental session question routes." }, { @@ -36942,8 +25114,22 @@ "description": "Project copy management routes." }, { - "name": "pty", - "description": "PTY websocket route." + "name": "vcs", + "description": "Location-scoped version control routes." + }, + { + "name": "debug" + }, + { + "name": "migration" + }, + { + "name": "websearch", + "description": "Location-scoped web search routes." + }, + { + "name": "config", + "description": "Location-scoped configuration routes." } ] } diff --git a/src/OpenCode.Sdk/.generated-manifest.json b/src/OpenCode.Sdk/.generated-manifest.json index 185076f45..e68c919aa 100644 --- a/src/OpenCode.Sdk/.generated-manifest.json +++ b/src/OpenCode.Sdk/.generated-manifest.json @@ -1,42 +1,65 @@ { "files": [ - "Internal/Serialization/LlmToolContentJsonConverter.cs", + "HealthResponse.cs", + "Internal/ResponseAdapters/HealthResponseAdapter.cs", + "Internal/ResponseAdapters/SessionMessageResponseAdapter.cs", "Internal/Serialization/OpenCodeErrorJsonConverter.cs", "Internal/Serialization/OpenCodeJsonContext.cs", + "Internal/Serialization/OptionalCollectionInput.cs", + "Internal/Serialization/PromptFileSourceJsonConverter.cs", "Internal/Serialization/SessionMessageAssistantContentJsonConverter.cs", - "Internal/Serialization/SessionMessageJsonConverter.cs", + "Internal/Serialization/SessionMessageCompactionJsonConverter.cs", + "Internal/Serialization/SessionMessageInfoJsonConverter.cs", "Internal/Serialization/SessionMessageToolStateJsonConverter.cs", + "Internal/Serialization/ToolContentJsonConverter.cs", "Models/InvalidRequestError.cs", - "Models/LlmToolContent.cs", + "Models/LocationRef.cs", "Models/MessageNotFoundError.cs", "Models/ModelRef.cs", "Models/OpenCodeError.cs", "Models/PromptAgentAttachment.cs", "Models/PromptFileAttachment.cs", - "Models/PromptSource.cs", - "Models/SessionErrorUnknown.cs", - "Models/SessionMessage.cs", - "Models/SessionMessageAgentSwitched.cs", - "Models/SessionMessageAgentSwitchedTime.cs", + "Models/PromptFileSource.cs", + "Models/PromptFileSourceInline.cs", + "Models/PromptFileSourceUri.cs", + "Models/PromptMention.cs", + "Models/PromptSkillAttachment.cs", + "Models/ServiceHealth.cs", + "Models/SessionMessageAgentSelected.cs", + "Models/SessionMessageAgentSelectedTime.cs", "Models/SessionMessageAssistant.cs", "Models/SessionMessageAssistantContent.cs", + "Models/SessionMessageAssistantFinish.cs", "Models/SessionMessageAssistantReasoning.cs", "Models/SessionMessageAssistantReasoningTime.cs", + "Models/SessionMessageAssistantRetry.cs", "Models/SessionMessageAssistantSnapshot.cs", "Models/SessionMessageAssistantText.cs", "Models/SessionMessageAssistantTime.cs", - "Models/SessionMessageAssistantTokens.cs", - "Models/SessionMessageAssistantTokensCache.cs", "Models/SessionMessageAssistantTool.cs", - "Models/SessionMessageAssistantToolProvider.cs", "Models/SessionMessageAssistantToolTime.cs", "Models/SessionMessageCompaction.cs", - "Models/SessionMessageCompactionReason.cs", - "Models/SessionMessageCompactionTime.cs", - "Models/SessionMessageModelSwitched.cs", - "Models/SessionMessageModelSwitchedTime.cs", + "Models/SessionMessageCompactionCompleted.cs", + "Models/SessionMessageCompactionCompletedReason.cs", + "Models/SessionMessageCompactionCompletedTime.cs", + "Models/SessionMessageCompactionFailed.cs", + "Models/SessionMessageCompactionFailedReason.cs", + "Models/SessionMessageCompactionFailedTime.cs", + "Models/SessionMessageCompactionRunning.cs", + "Models/SessionMessageCompactionRunningReason.cs", + "Models/SessionMessageCompactionRunningTime.cs", + "Models/SessionMessageInfo.cs", + "Models/SessionMessageLocationSwitched.cs", + "Models/SessionMessageLocationSwitchedPrevious.cs", + "Models/SessionMessageLocationSwitchedTime.cs", + "Models/SessionMessageModelSelected.cs", + "Models/SessionMessageModelSelectedTime.cs", "Models/SessionMessageShell.cs", + "Models/SessionMessageShellOutput.cs", + "Models/SessionMessageShellStatus.cs", "Models/SessionMessageShellTime.cs", + "Models/SessionMessageSkill.cs", + "Models/SessionMessageSkillTime.cs", "Models/SessionMessageSynthetic.cs", "Models/SessionMessageSyntheticTime.cs", "Models/SessionMessageSystem.cs", @@ -44,18 +67,29 @@ "Models/SessionMessageToolState.cs", "Models/SessionMessageToolStateCompleted.cs", "Models/SessionMessageToolStateError.cs", - "Models/SessionMessageToolStatePending.cs", "Models/SessionMessageToolStateRunning.cs", + "Models/SessionMessageToolStateStreaming.cs", "Models/SessionMessageUser.cs", "Models/SessionMessageUserTime.cs", "Models/SessionNotFoundError.cs", + "Models/SessionStructuredError.cs", + "Models/TokenUsageInfo.cs", + "Models/TokenUsageInfoCache.cs", + "Models/ToolContent.cs", "Models/ToolFileContent.cs", "Models/ToolTextContent.cs", "Models/UnauthorizedError.cs", - "Models/UnknownLlmToolContent.cs", "Models/UnknownOpenCodeError.cs", - "Models/UnknownSessionMessage.cs", + "Models/UnknownPromptFileSource.cs", "Models/UnknownSessionMessageAssistantContent.cs", - "Models/UnknownSessionMessageToolState.cs" + "Models/UnknownSessionMessageCompaction.cs", + "Models/UnknownSessionMessageInfo.cs", + "Models/UnknownSessionMessageToolState.cs", + "Models/UnknownToolContent.cs", + "OpenCodeClient.cs", + "OpenCodeRoutes.cs", + "SessionClient.cs", + "SessionMessageResponse.cs", + "SessionsClient.cs" ] } diff --git a/src/OpenCode.Sdk/.generation-incomplete b/src/OpenCode.Sdk/.generation-incomplete index c14349334..a97248a44 100644 --- a/src/OpenCode.Sdk/.generation-incomplete +++ b/src/OpenCode.Sdk/.generation-incomplete @@ -1,38 +1,61 @@ Generation is incomplete; packages must not be published. Selected operations: 2 -Pending modern operations: 59 -Pending legacy operations: 127 +Pending operations: 118 Selected: - v2.health.get - v2.session.message -Pending modern: +Pending: +- v2.agent.get - v2.agent.list - v2.command.list +- v2.config.get - v2.credential.remove - v2.credential.update +- v2.debug.location.evict +- v2.debug.location.list - v2.event.subscribe +- v2.experimental.integration.wellknown.add +- v2.experimental.migration.v1.status +- v2.form.request.list - v2.fs.find - v2.fs.list - v2.fs.read -- v2.integration.attempt.cancel -- v2.integration.attempt.complete -- v2.integration.attempt.status +- v2.generate.text +- v2.health.stop +- v2.integration.command.cancel +- v2.integration.command.connect +- v2.integration.command.status - v2.integration.connect.key -- v2.integration.connect.oauth - v2.integration.get - v2.integration.list +- v2.integration.oauth.cancel +- v2.integration.oauth.complete +- v2.integration.oauth.connect +- v2.integration.oauth.status - v2.location.get +- v2.mcp.add +- v2.mcp.connect +- v2.mcp.disconnect +- v2.mcp.list +- v2.mcp.remove +- v2.mcp.resource.catalog +- v2.message.list +- v2.model.default - v2.model.list - v2.permission.request.list - v2.permission.saved.list - v2.permission.saved.remove +- v2.plugin.list +- v2.project.current +- v2.project.directories +- v2.project.list - v2.projectCopy.create - v2.projectCopy.refresh - v2.projectCopy.remove - v2.provider.get - v2.provider.list - v2.pty.connect -- v2.pty.connectToken +- v2.pty.connect.token - v2.pty.create - v2.pty.get - v2.pty.list @@ -40,16 +63,35 @@ Pending modern: - v2.pty.update - v2.question.request.list - v2.reference.list +- v2.server.get - v2.session.active +- v2.session.background +- v2.session.command - v2.session.compact - v2.session.context - v2.session.create -- v2.session.events +- v2.session.export +- v2.session.fork +- v2.session.form.cancel +- v2.session.form.create +- v2.session.form.get +- v2.session.form.list +- v2.session.form.reply +- v2.session.form.state +- v2.session.generate - v2.session.get -- v2.session.history +- v2.session.import +- v2.session.inbox.cancel +- v2.session.inbox.list +- v2.session.inbox.queue +- v2.session.inbox.steer +- v2.session.instructions.entry.list +- v2.session.instructions.entry.put +- v2.session.instructions.entry.remove - v2.session.interrupt - v2.session.list -- v2.session.messages +- v2.session.log +- v2.session.move - v2.session.permission.create - v2.session.permission.get - v2.session.permission.list @@ -58,138 +100,26 @@ Pending modern: - v2.session.question.list - v2.session.question.reject - v2.session.question.reply +- v2.session.remove +- v2.session.rename - v2.session.revert.clear - v2.session.revert.commit - v2.session.revert.stage +- v2.session.shell +- v2.session.skill - v2.session.switchAgent - v2.session.switchModel +- v2.session.synthetic - v2.session.wait +- v2.shell.create +- v2.shell.get +- v2.shell.list +- v2.shell.output +- v2.shell.remove +- v2.shell.timeout - v2.skill.list -Pending legacy: -- app.agents -- app.log -- app.skills -- auth.remove -- auth.set -- command.list -- config.get -- config.providers -- config.update -- event.subscribe -- experimental.capabilities.get -- experimental.console.get -- experimental.console.listOrgs -- experimental.console.switchOrg -- experimental.controlPlane.moveSession -- experimental.projectCopy.generateName -- experimental.resource.list -- experimental.session.background -- experimental.session.list -- experimental.workspace.adapter.list -- experimental.workspace.create -- experimental.workspace.list -- experimental.workspace.remove -- experimental.workspace.status -- experimental.workspace.syncList -- experimental.workspace.warp -- file.list -- file.read -- file.status -- find.files -- find.symbols -- find.text -- formatter.status -- global.config.get -- global.config.update -- global.dispose -- global.event -- global.health -- global.upgrade -- instance.dispose -- lsp.status -- mcp.add -- mcp.auth.authenticate -- mcp.auth.callback -- mcp.auth.remove -- mcp.auth.start -- mcp.connect -- mcp.disconnect -- mcp.status -- part.delete -- part.update -- path.get -- permission.list -- permission.reply -- permission.respond -- project.current -- project.directories -- project.initGit -- project.list -- project.update -- provider.auth -- provider.list -- provider.oauth.authorize -- provider.oauth.callback -- pty.connect -- pty.connectToken -- pty.create -- pty.get -- pty.list -- pty.remove -- pty.shells -- pty.update -- question.list -- question.reject -- question.reply -- session.abort -- session.children -- session.command -- session.create -- session.delete -- session.deleteMessage -- session.diff -- session.fork -- session.get -- session.init -- session.list -- session.message -- session.messages -- session.prompt -- session.prompt_async -- session.revert -- session.share -- session.shell -- session.status -- session.summarize -- session.todo -- session.unrevert -- session.unshare -- session.update -- sync.history.list -- sync.replay -- sync.start -- sync.steal -- tool.ids -- tool.list -- tui.appendPrompt -- tui.clearPrompt -- tui.control.next -- tui.control.response -- tui.executeCommand -- tui.openHelp -- tui.openModels -- tui.openSessions -- tui.openThemes -- tui.publish -- tui.selectSession -- tui.showToast -- tui.submitPrompt -- vcs.apply -- vcs.diff -- vcs.diff.raw -- vcs.get -- vcs.status -- worktree.create -- worktree.list -- worktree.remove -- worktree.reset +- v2.vcs.diff +- v2.vcs.get +- v2.vcs.status +- v2.websearch.providers +- v2.websearch.query diff --git a/src/OpenCode.Sdk/ErrorBehavior.cs b/src/OpenCode.Sdk/ErrorBehavior.cs new file mode 100644 index 000000000..fc03561ac --- /dev/null +++ b/src/OpenCode.Sdk/ErrorBehavior.cs @@ -0,0 +1,11 @@ +namespace OpenCode.Sdk; + +/// Selects how a single call surfaces opencode API error responses. +public enum ErrorBehavior +{ + /// API error responses throw . + Default = 0, + + /// API error responses return on the response envelope instead of throwing. + NoThrow = 1, +} diff --git a/src/OpenCode.Sdk/HealthResponse.cs b/src/OpenCode.Sdk/HealthResponse.cs new file mode 100644 index 000000000..8cdbaa278 --- /dev/null +++ b/src/OpenCode.Sdk/HealthResponse.cs @@ -0,0 +1,59 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Diagnostics.CodeAnalysis; +using System.Text; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk; +/// +/// Represents the response of the 'GET /api/health' operation. +/// +public sealed record HealthResponse : OpenCodeResponse +{ + private readonly ServiceHealth? _health; + /// + /// Initializes a success instance of the 'HealthResponse' envelope. + /// + public HealthResponse() + { + } + + /// + /// Initializes an error-path instance; the payload stays unset behind its guard. + /// + [SetsRequiredMembers] + internal HealthResponse(int status, OpenCodeError? error, string? rawBody) + { + Status = status; + IsError = true; + Error = error; + RawBody = rawBody; + Health = null!; + } + + /// + /// Gets the Health payload; guarded on the error path. + /// + public required ServiceHealth Health { get => _health ?? throw new InvalidOperationException("The response is an error; check IsError before accessing Health."); init => _health = value; } + + /// + /// Prints the shared metadata and appends the payload only when it is present. + /// + protected override bool PrintMembers(StringBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + var printed = base.PrintMembers(builder); + if (_health is null) + { + return printed; + } + + if (printed) + { + _ = builder.Append(", "); + } + + _ = builder.Append("Health = ").Append(_health); + return true; + } +} diff --git a/src/OpenCode.Sdk/Internal/Abstractions/IEnvironmentProvider.cs b/src/OpenCode.Sdk/Internal/Abstractions/IEnvironmentProvider.cs new file mode 100644 index 000000000..8996eccd2 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Abstractions/IEnvironmentProvider.cs @@ -0,0 +1,10 @@ +namespace OpenCode.Sdk.Internal.Abstractions; + +/// Reads process environment variables behind a substitutable seam. +internal interface IEnvironmentProvider +{ + /// Reads one environment variable. + /// The variable name. + /// The value, or when the variable is not set. + public string? GetEnvironmentVariable(string name); +} diff --git a/src/OpenCode.Sdk/Internal/EndpointPolicy.cs b/src/OpenCode.Sdk/Internal/EndpointPolicy.cs new file mode 100644 index 000000000..fedae3035 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/EndpointPolicy.cs @@ -0,0 +1,40 @@ +namespace OpenCode.Sdk.Internal; + +/// Validates a server endpoint and normalizes it into the request base. +internal static class EndpointPolicy +{ + /// Validates the endpoint and returns the base every route is appended to. + /// The configured server endpoint. + /// The normalized base: scheme, authority, and path prefix without a trailing slash. + public static string Normalize(Uri endpoint) + { + ArgumentNullException.ThrowIfNull(endpoint); + + if (!endpoint.IsAbsoluteUri) + { + throw new ArgumentException("The endpoint must be an absolute URI.", nameof(endpoint)); + } + + if (endpoint.Scheme is not ("http" or "https")) + { + throw new ArgumentException("The endpoint scheme must be HTTP or HTTPS.", nameof(endpoint)); + } + + if (!string.IsNullOrEmpty(endpoint.Query)) + { + throw new ArgumentException("The endpoint must not carry a query.", nameof(endpoint)); + } + + if (!string.IsNullOrEmpty(endpoint.Fragment)) + { + throw new ArgumentException("The endpoint must not carry a fragment.", nameof(endpoint)); + } + + if (!string.IsNullOrEmpty(endpoint.UserInfo)) + { + throw new ArgumentException("The endpoint must not carry user information.", nameof(endpoint)); + } + + return $"{endpoint.Scheme}://{endpoint.Authority}{endpoint.AbsolutePath.TrimEnd('/')}"; + } +} diff --git a/src/OpenCode.Sdk/Internal/MockSeam.cs b/src/OpenCode.Sdk/Internal/MockSeam.cs new file mode 100644 index 000000000..f6e47a5a8 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/MockSeam.cs @@ -0,0 +1,14 @@ +namespace OpenCode.Sdk.Internal; + +/// Builds the instructive failure every unoverridden mock-seam member throws. +internal static class MockSeam +{ + public static InvalidOperationException CreateError(string typeName, string memberName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(typeName); + ArgumentException.ThrowIfNullOrWhiteSpace(memberName); + + return new InvalidOperationException( + $"'{typeName}.{memberName}' was invoked on an instance created through the protected mocking constructor; override the member under test."); + } +} diff --git a/src/OpenCode.Sdk/Internal/Pipeline.cs b/src/OpenCode.Sdk/Internal/Pipeline.cs new file mode 100644 index 000000000..177806a60 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Pipeline.cs @@ -0,0 +1,173 @@ +using System.Globalization; +using System.Net.Http.Headers; +using System.Text; +using OpenCode.Sdk.Internal.Abstractions; + +namespace OpenCode.Sdk.Internal; + +/// Owns request decoration, sending, buffering, and error-channel policy for every operation. +internal sealed class Pipeline : IDisposable +{ + private const string BasicUser = "opencode"; + private const string PasswordVariable = "OPENCODE_SERVER_PASSWORD"; + + private readonly AuthenticationHeaderValue? _authorization; + private readonly string _endpointBase; + private readonly HttpClient _httpClient; + private readonly bool _ownsHttpClient; + private readonly ProductInfoHeaderValue _userAgent; + private bool _disposed; + + internal Pipeline(HttpClient httpClient, bool ownsHttpClient, Uri endpoint, string? password, IEnvironmentProvider environment) + { + ArgumentNullException.ThrowIfNull(httpClient); + ArgumentNullException.ThrowIfNull(endpoint); + ArgumentNullException.ThrowIfNull(environment); + + _httpClient = httpClient; + _ownsHttpClient = ownsHttpClient; + _endpointBase = EndpointPolicy.Normalize(endpoint); + _userAgent = UserAgentPolicy.Resolve(); + + // The environment fallback is read exactly once, here; requests reuse the resolved header. + var resolvedPassword = string.IsNullOrEmpty(password) ? environment.GetEnvironmentVariable(PasswordVariable) : password; + _authorization = string.IsNullOrEmpty(resolvedPassword) + ? null + : new AuthenticationHeaderValue( + "Basic", + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{BasicUser}:{resolvedPassword}"))); + } + + public static Pipeline Create(Uri endpoint, OpenCodeClientOptions? options, IEnvironmentProvider environment) + { + ArgumentNullException.ThrowIfNull(endpoint); + ArgumentNullException.ThrowIfNull(environment); + if (options?.Endpoint is not null) + { + throw new ArgumentException( + "The endpoint constructor owns endpoint authority; leave OpenCodeClientOptions.Endpoint unset.", + nameof(options)); + } + + // Validate before constructing the owned client so a refused endpoint leaks nothing. + _ = EndpointPolicy.Normalize(endpoint); + return new Pipeline(new HttpClient(), ownsHttpClient: true, endpoint, options?.Password, environment); + } + + public static Pipeline Create(HttpClient httpClient, OpenCodeClientOptions options, IEnvironmentProvider environment) + { + ArgumentNullException.ThrowIfNull(httpClient); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(environment); + if (options.Endpoint is null) + { + throw new ArgumentException( + "A caller-supplied HttpClient requires OpenCodeClientOptions.Endpoint.", + nameof(options)); + } + + return new Pipeline(httpClient, ownsHttpClient: false, options.Endpoint, options.Password, environment); + } + + public async Task ExecuteAsync(HttpMethod method, string route, ResponseAdapter adapter, + OpenCodeRequestOptions? options, CancellationToken cancellationToken) + where TResponse : OpenCodeResponse + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(method); + ArgumentException.ThrowIfNullOrWhiteSpace(route); + ArgumentNullException.ThrowIfNull(adapter); + if (route[0] is not '/') + { + throw new ArgumentException("Routes must start with '/'.", nameof(route)); + } + + // Refused before sending: an undefined value must never silently pick an error channel. + var errorBehavior = options?.ErrorBehavior ?? ErrorBehavior.Default; + if (errorBehavior is not (ErrorBehavior.Default or ErrorBehavior.NoThrow)) + { + throw new ArgumentOutOfRangeException(nameof(options), errorBehavior, "Unknown ErrorBehavior value."); + } + + int status; + string rawBody; + using (var request = new HttpRequestMessage(method, new Uri(_endpointBase + route, UriKind.Absolute))) + { + Decorate(request); + using var response = await SendCoreAsync(request, cancellationToken).ConfigureAwait(false); + status = (int)response.StatusCode; + rawBody = await ReadBodyAsync(response, cancellationToken).ConfigureAwait(false); + } + + var adapted = adapter.Adapt(status, rawBody); + return adapted.IsError && errorBehavior is ErrorBehavior.Default + ? throw CreateApiException(adapted) + : adapted; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + if (_ownsHttpClient) + { + _httpClient.Dispose(); + } + } + + private void Decorate(HttpRequestMessage request) + { + if (_authorization is not null) + { + request.Headers.Authorization = _authorization; + } + + request.Headers.UserAgent.Add(_userAgent); + } + + private async Task SendCoreAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + try + { + return await _httpClient + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + } + catch (Exception exception) when (exception is HttpRequestException or ObjectDisposedException) + { + // ObjectDisposedException covers a dispose-during-send race; the pre-send disposed + // guard stays outside this catch. OperationCanceledException passes through untouched. + throw new OpenCodeTransportException("The opencode server could not be reached.", exception); + } + } + + private static async Task ReadBodyAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + try + { + return response.Content is null + ? string.Empty + : await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (exception is HttpRequestException or IOException or ObjectDisposedException + or InvalidOperationException) + { + // InvalidOperationException covers an unusable response charset surfaced by + // ReadAsStringAsync. OperationCanceledException deliberately passes through untouched. + throw new OpenCodeTransportException("The opencode response body could not be read.", exception); + } + } + + private static OpenCodeApiException CreateApiException(OpenCodeResponse response) + { + var status = response.Status.ToString(CultureInfo.InvariantCulture); + var message = response.Error is null + ? $"The opencode API returned status {status}." + : $"The opencode API returned status {status} ('{response.Error.Tag}')."; + return new OpenCodeApiException(message, response.Status, response.Error, response.RawBody); + } +} diff --git a/src/OpenCode.Sdk/Internal/ResponseAdapter.cs b/src/OpenCode.Sdk/Internal/ResponseAdapter.cs new file mode 100644 index 000000000..6f882b27a --- /dev/null +++ b/src/OpenCode.Sdk/Internal/ResponseAdapter.cs @@ -0,0 +1,111 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using OpenCode.Sdk.Internal.Serialization; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk.Internal; + +/// Maps one buffered HTTP response onto an operation's typed envelope. +/// The operation's response envelope type. +internal abstract class ResponseAdapter + where TResponse : OpenCodeResponse +{ + /// Maps a buffered response body onto the typed envelope. + /// The HTTP status code. + /// The buffered response body. + /// The typed envelope; error responses set . + public abstract TResponse Adapt(int status, string rawBody); + + /// Reads a bare success payload; a malformed body is a protocol failure. + /// The payload type. + /// The buffered success body. + /// The source-generated payload metadata. + /// The payload. + protected static TPayload ReadBarePayload(string rawBody, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(rawBody); + ArgumentNullException.ThrowIfNull(typeInfo); + + try + { + return JsonSerializer.Deserialize(rawBody, typeInfo) + ?? throw new OpenCodeTransportException("The opencode API returned a null success body."); + } + catch (JsonException exception) + { + throw new OpenCodeTransportException("The opencode API returned a malformed success body.", exception); + } + } + + /// Reads a {"data": ...} success payload; a malformed envelope is a protocol failure. + /// The payload type. + /// The buffered success body. + /// The source-generated payload metadata. + /// The payload. + protected static TPayload ReadDataPayload(string rawBody, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(rawBody); + ArgumentNullException.ThrowIfNull(typeInfo); + + try + { + using var document = JsonDocument.Parse(rawBody); + if (document.RootElement.ValueKind is not JsonValueKind.Object + || !document.RootElement.TryGetProperty("data", out var data)) + { + throw new OpenCodeTransportException("The opencode API returned a success body without its 'data' envelope."); + } + + return JsonSerializer.Deserialize(data, typeInfo) + ?? throw new OpenCodeTransportException("The opencode API returned a null success payload."); + } + catch (JsonException exception) + { + throw new OpenCodeTransportException("The opencode API returned a malformed success body.", exception); + } + } + + /// Builds the protocol failure for a success status the operation does not declare. + /// The undeclared 2xx status code. + /// The transport failure the adapter throws. + protected static OpenCodeTransportException UndeclaredSuccessFailure(int status) => + new($"The opencode API returned undeclared success status {status.ToString(CultureInfo.InvariantCulture)}."); + + /// + /// Reads a typed error tolerantly: malformed JSON yields so the raw + /// body remains the only record; an unknown tag keeps its carrier; a known tag outside the + /// operation's status map — or on an undeclared status, when + /// is — downgrades to the unknown carrier so the operation contract + /// never widens. + /// + /// The buffered error body. + /// The tags the status map declares for this status, or for an undeclared status. + /// The typed error, or when the body could not be parsed. + protected static OpenCodeError? ReadTolerantError(string rawBody, IReadOnlyCollection? allowedTags) + { + ArgumentNullException.ThrowIfNull(rawBody); + + try + { + var error = JsonSerializer.Deserialize(rawBody, OpenCodeJsonContext.Default.OpenCodeError); + return error switch + { + null => null, + UnknownOpenCodeError unknown => unknown, + _ when allowedTags is not null && allowedTags.Contains(error.Tag, StringComparer.Ordinal) => error, + _ => Downgrade(error.Tag, rawBody), + }; + } + catch (JsonException) + { + return null; + } + } + + private static UnknownOpenCodeError Downgrade(string tag, string rawBody) + { + using var document = JsonDocument.Parse(rawBody); + return new UnknownOpenCodeError(tag, document.RootElement); + } +} diff --git a/src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs b/src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs new file mode 100644 index 000000000..0ae4f92b4 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs @@ -0,0 +1,41 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Internal.ResponseAdapters; +/// +/// Adapts the 'GET /api/health' responses onto 'HealthResponse'. +/// +internal sealed class HealthResponseAdapter : ResponseAdapter +{ + private static readonly string[] Status400Tags = ["InvalidRequestError"]; + private static readonly string[] Status401Tags = ["UnauthorizedError"]; + private HealthResponseAdapter() + { + } + + /// + /// Gets the shared adapter instance. + /// + public static HealthResponseAdapter Instance { get; } = new HealthResponseAdapter(); + + /// + /// Maps one buffered response onto the typed envelope. + /// + public override HealthResponse Adapt(int status, string rawBody) + { + ArgumentNullException.ThrowIfNull(rawBody); + return status switch + { + 200 => new HealthResponse + { + Status = status, + Health = ReadBarePayload(rawBody, OpenCodeJsonContext.Default.ServiceHealth) + }, + >= 200 and < 300 => throw UndeclaredSuccessFailure(status), + 400 => new HealthResponse(status, ReadTolerantError(rawBody, Status400Tags), rawBody), + 401 => new HealthResponse(status, ReadTolerantError(rawBody, Status401Tags), rawBody), + _ => new HealthResponse(status, ReadTolerantError(rawBody, null), rawBody) + }; + } +} diff --git a/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageResponseAdapter.cs b/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageResponseAdapter.cs new file mode 100644 index 000000000..2f47d645c --- /dev/null +++ b/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageResponseAdapter.cs @@ -0,0 +1,43 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Internal.ResponseAdapters; +/// +/// Adapts the 'GET /api/session/{sessionID}/message/{messageID}' responses onto 'SessionMessageResponse'. +/// +internal sealed class SessionMessageResponseAdapter : ResponseAdapter +{ + private static readonly string[] Status400Tags = ["InvalidRequestError"]; + private static readonly string[] Status401Tags = ["UnauthorizedError"]; + private static readonly string[] Status404Tags = ["MessageNotFoundError", "SessionNotFoundError"]; + private SessionMessageResponseAdapter() + { + } + + /// + /// Gets the shared adapter instance. + /// + public static SessionMessageResponseAdapter Instance { get; } = new SessionMessageResponseAdapter(); + + /// + /// Maps one buffered response onto the typed envelope. + /// + public override SessionMessageResponse Adapt(int status, string rawBody) + { + ArgumentNullException.ThrowIfNull(rawBody); + return status switch + { + 200 => new SessionMessageResponse + { + Status = status, + Message = ReadDataPayload(rawBody, OpenCodeJsonContext.Default.SessionMessageInfo) + }, + >= 200 and < 300 => throw UndeclaredSuccessFailure(status), + 400 => new SessionMessageResponse(status, ReadTolerantError(rawBody, Status400Tags), rawBody), + 401 => new SessionMessageResponse(status, ReadTolerantError(rawBody, Status401Tags), rawBody), + 404 => new SessionMessageResponse(status, ReadTolerantError(rawBody, Status404Tags), rawBody), + _ => new SessionMessageResponse(status, ReadTolerantError(rawBody, null), rawBody) + }; + } +} diff --git a/src/OpenCode.Sdk/Internal/Serialization/OpenCodeErrorJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeErrorJsonConverter.cs index c90778bce..e4f7cc019 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/OpenCodeErrorJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeErrorJsonConverter.cs @@ -43,7 +43,12 @@ public override OpenCodeError Read(ref Utf8JsonReader reader, Type typeToConvert throw new JsonException("The '_tag' marker must be a string."); } - var marker = markerElement.GetString() ?? throw new JsonException("The '_tag' marker cannot be null."); + var marker = markerElement.GetString(); + if (marker is null || string.IsNullOrWhiteSpace(marker)) + { + throw new JsonException("The '_tag' marker must be a non-empty string."); + } + if (TypesByTag.TryGetValue(marker, out var targetType)) { var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for OpenCodeError."); diff --git a/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs index 2d9e85760..560f5924b 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs @@ -7,36 +7,53 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata, RespectNullableAnnotations = true)] [JsonSerializable(typeof(InvalidRequestError))] -[JsonSerializable(typeof(LlmToolContent))] +[JsonSerializable(typeof(LocationRef))] [JsonSerializable(typeof(MessageNotFoundError))] [JsonSerializable(typeof(ModelRef))] [JsonSerializable(typeof(OpenCodeError))] [JsonSerializable(typeof(PromptAgentAttachment))] [JsonSerializable(typeof(PromptFileAttachment))] -[JsonSerializable(typeof(PromptSource))] -[JsonSerializable(typeof(SessionErrorUnknown))] -[JsonSerializable(typeof(SessionMessage))] -[JsonSerializable(typeof(SessionMessageAgentSwitched))] -[JsonSerializable(typeof(SessionMessageAgentSwitchedTime))] +[JsonSerializable(typeof(PromptFileSource))] +[JsonSerializable(typeof(PromptFileSourceInline))] +[JsonSerializable(typeof(PromptFileSourceUri))] +[JsonSerializable(typeof(PromptMention))] +[JsonSerializable(typeof(PromptSkillAttachment))] +[JsonSerializable(typeof(ServiceHealth))] +[JsonSerializable(typeof(SessionMessageAgentSelected))] +[JsonSerializable(typeof(SessionMessageAgentSelectedTime))] [JsonSerializable(typeof(SessionMessageAssistant))] [JsonSerializable(typeof(SessionMessageAssistantContent))] +[JsonSerializable(typeof(SessionMessageAssistantFinish))] [JsonSerializable(typeof(SessionMessageAssistantReasoning))] [JsonSerializable(typeof(SessionMessageAssistantReasoningTime))] +[JsonSerializable(typeof(SessionMessageAssistantRetry))] [JsonSerializable(typeof(SessionMessageAssistantSnapshot))] [JsonSerializable(typeof(SessionMessageAssistantText))] [JsonSerializable(typeof(SessionMessageAssistantTime))] -[JsonSerializable(typeof(SessionMessageAssistantTokens))] -[JsonSerializable(typeof(SessionMessageAssistantTokensCache))] [JsonSerializable(typeof(SessionMessageAssistantTool))] -[JsonSerializable(typeof(SessionMessageAssistantToolProvider))] [JsonSerializable(typeof(SessionMessageAssistantToolTime))] [JsonSerializable(typeof(SessionMessageCompaction))] -[JsonSerializable(typeof(SessionMessageCompactionReason))] -[JsonSerializable(typeof(SessionMessageCompactionTime))] -[JsonSerializable(typeof(SessionMessageModelSwitched))] -[JsonSerializable(typeof(SessionMessageModelSwitchedTime))] +[JsonSerializable(typeof(SessionMessageCompactionCompleted))] +[JsonSerializable(typeof(SessionMessageCompactionCompletedReason))] +[JsonSerializable(typeof(SessionMessageCompactionCompletedTime))] +[JsonSerializable(typeof(SessionMessageCompactionFailed))] +[JsonSerializable(typeof(SessionMessageCompactionFailedReason))] +[JsonSerializable(typeof(SessionMessageCompactionFailedTime))] +[JsonSerializable(typeof(SessionMessageCompactionRunning))] +[JsonSerializable(typeof(SessionMessageCompactionRunningReason))] +[JsonSerializable(typeof(SessionMessageCompactionRunningTime))] +[JsonSerializable(typeof(SessionMessageInfo))] +[JsonSerializable(typeof(SessionMessageLocationSwitched))] +[JsonSerializable(typeof(SessionMessageLocationSwitchedPrevious))] +[JsonSerializable(typeof(SessionMessageLocationSwitchedTime))] +[JsonSerializable(typeof(SessionMessageModelSelected))] +[JsonSerializable(typeof(SessionMessageModelSelectedTime))] [JsonSerializable(typeof(SessionMessageShell))] +[JsonSerializable(typeof(SessionMessageShellOutput))] +[JsonSerializable(typeof(SessionMessageShellStatus))] [JsonSerializable(typeof(SessionMessageShellTime))] +[JsonSerializable(typeof(SessionMessageSkill))] +[JsonSerializable(typeof(SessionMessageSkillTime))] [JsonSerializable(typeof(SessionMessageSynthetic))] [JsonSerializable(typeof(SessionMessageSyntheticTime))] [JsonSerializable(typeof(SessionMessageSystem))] @@ -44,19 +61,25 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSerializable(typeof(SessionMessageToolState))] [JsonSerializable(typeof(SessionMessageToolStateCompleted))] [JsonSerializable(typeof(SessionMessageToolStateError))] -[JsonSerializable(typeof(SessionMessageToolStatePending))] [JsonSerializable(typeof(SessionMessageToolStateRunning))] +[JsonSerializable(typeof(SessionMessageToolStateStreaming))] [JsonSerializable(typeof(SessionMessageUser))] [JsonSerializable(typeof(SessionMessageUserTime))] [JsonSerializable(typeof(SessionNotFoundError))] +[JsonSerializable(typeof(SessionStructuredError))] +[JsonSerializable(typeof(TokenUsageInfo))] +[JsonSerializable(typeof(TokenUsageInfoCache))] +[JsonSerializable(typeof(ToolContent))] [JsonSerializable(typeof(ToolFileContent))] [JsonSerializable(typeof(ToolTextContent))] [JsonSerializable(typeof(UnauthorizedError))] -[JsonSerializable(typeof(UnknownLlmToolContent))] [JsonSerializable(typeof(UnknownOpenCodeError))] -[JsonSerializable(typeof(UnknownSessionMessage))] +[JsonSerializable(typeof(UnknownPromptFileSource))] [JsonSerializable(typeof(UnknownSessionMessageAssistantContent))] +[JsonSerializable(typeof(UnknownSessionMessageCompaction))] +[JsonSerializable(typeof(UnknownSessionMessageInfo))] [JsonSerializable(typeof(UnknownSessionMessageToolState))] +[JsonSerializable(typeof(UnknownToolContent))] internal sealed partial class OpenCodeJsonContext : JsonSerializerContext { } diff --git a/src/OpenCode.Sdk/Internal/Serialization/OptionalCollectionInput.cs b/src/OpenCode.Sdk/Internal/Serialization/OptionalCollectionInput.cs new file mode 100644 index 000000000..5e22f011e --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Serialization/OptionalCollectionInput.cs @@ -0,0 +1,9 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +namespace OpenCode.Sdk.Internal.Serialization; + +internal static class OptionalCollectionInput +{ + internal static T Normalize(T? value, T empty, Func copy) + where T : class => value is null ? empty : copy(value); +} diff --git a/src/OpenCode.Sdk/Internal/Serialization/PromptFileSourceJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/PromptFileSourceJsonConverter.cs new file mode 100644 index 000000000..8226602ee --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Serialization/PromptFileSourceJsonConverter.cs @@ -0,0 +1,73 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk.Internal.Serialization; + +internal sealed class PromptFileSourceJsonConverter : JsonConverter +{ + private static readonly Dictionary TypesByTag = new(StringComparer.Ordinal) + { + ["inline"] = typeof(PromptFileSourceInline), + ["uri"] = typeof(PromptFileSourceUri) + }; + public override bool HandleNull => true; + + public override PromptFileSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(typeToConvert); + ArgumentNullException.ThrowIfNull(options); + if (reader.TokenType == JsonTokenType.Null) + { + throw new JsonException("The PromptFileSource payload cannot be null."); + } + + using var document = JsonDocument.ParseValue(ref reader); + var payload = document.RootElement; + if (payload.ValueKind != JsonValueKind.Object) + { + throw new JsonException("The PromptFileSource payload must be a JSON object."); + } + + if (!payload.TryGetProperty("type", out var markerElement)) + { + throw new JsonException("The PromptFileSource payload must contain 'type'."); + } + + if (markerElement.ValueKind != JsonValueKind.String) + { + throw new JsonException("The 'type' marker must be a string."); + } + + var marker = markerElement.GetString(); + if (marker is null || string.IsNullOrWhiteSpace(marker)) + { + throw new JsonException("The 'type' marker must be a non-empty string."); + } + + if (TypesByTag.TryGetValue(marker, out var targetType)) + { + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for PromptFileSource."); + return JsonSerializer.Deserialize(payload, typeInfo) as PromptFileSource ?? throw new JsonException("The PromptFileSource payload deserialized to null."); + } + + return new UnknownPromptFileSource(marker, payload); + } + + public override void Write(Utf8JsonWriter writer, PromptFileSource value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(options); + if (value is UnknownPromptFileSource unknown) + { + unknown.Payload.WriteTo(writer); + return; + } + + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(value.GetType()) ?? throw new JsonException("The generated context has no metadata for PromptFileSource."); + JsonSerializer.Serialize(writer, value, typeInfo); + } +} diff --git a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantContentJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantContentJsonConverter.cs index aca524b04..272c6b754 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantContentJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantContentJsonConverter.cs @@ -42,7 +42,12 @@ public override SessionMessageAssistantContent Read(ref Utf8JsonReader reader, T throw new JsonException("The 'type' marker must be a string."); } - var marker = markerElement.GetString() ?? throw new JsonException("The 'type' marker cannot be null."); + var marker = markerElement.GetString(); + if (marker is null || string.IsNullOrWhiteSpace(marker)) + { + throw new JsonException("The 'type' marker must be a non-empty string."); + } + if (TypesByTag.TryGetValue(marker, out var targetType)) { var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for SessionMessageAssistantContent."); diff --git a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageCompactionJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageCompactionJsonConverter.cs new file mode 100644 index 000000000..e8af70ce0 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageCompactionJsonConverter.cs @@ -0,0 +1,74 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk.Internal.Serialization; + +internal sealed class SessionMessageCompactionJsonConverter : JsonConverter +{ + private static readonly Dictionary TypesByTag = new(StringComparer.Ordinal) + { + ["completed"] = typeof(SessionMessageCompactionCompleted), + ["failed"] = typeof(SessionMessageCompactionFailed), + ["running"] = typeof(SessionMessageCompactionRunning) + }; + public override bool HandleNull => true; + + public override SessionMessageCompaction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(typeToConvert); + ArgumentNullException.ThrowIfNull(options); + if (reader.TokenType == JsonTokenType.Null) + { + throw new JsonException("The SessionMessageCompaction payload cannot be null."); + } + + using var document = JsonDocument.ParseValue(ref reader); + var payload = document.RootElement; + if (payload.ValueKind != JsonValueKind.Object) + { + throw new JsonException("The SessionMessageCompaction payload must be a JSON object."); + } + + if (!payload.TryGetProperty("status", out var markerElement)) + { + throw new JsonException("The SessionMessageCompaction payload must contain 'status'."); + } + + if (markerElement.ValueKind != JsonValueKind.String) + { + throw new JsonException("The 'status' marker must be a string."); + } + + var marker = markerElement.GetString(); + if (marker is null || string.IsNullOrWhiteSpace(marker)) + { + throw new JsonException("The 'status' marker must be a non-empty string."); + } + + if (TypesByTag.TryGetValue(marker, out var targetType)) + { + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for SessionMessageCompaction."); + return JsonSerializer.Deserialize(payload, typeInfo) as SessionMessageCompaction ?? throw new JsonException("The SessionMessageCompaction payload deserialized to null."); + } + + return new UnknownSessionMessageCompaction(marker, payload); + } + + public override void Write(Utf8JsonWriter writer, SessionMessageCompaction value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(options); + if (value is UnknownSessionMessageCompaction unknown) + { + unknown.Payload.WriteTo(writer); + return; + } + + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(value.GetType()) ?? throw new JsonException("The generated context has no metadata for SessionMessageCompaction."); + JsonSerializer.Serialize(writer, value, typeInfo); + } +} diff --git a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageInfoJsonConverter.cs similarity index 58% rename from src/OpenCode.Sdk/Internal/Serialization/SessionMessageJsonConverter.cs rename to src/OpenCode.Sdk/Internal/Serialization/SessionMessageInfoJsonConverter.cs index 68c5f8411..c2b4a2dc6 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageInfoJsonConverter.cs @@ -6,40 +6,42 @@ namespace OpenCode.Sdk.Internal.Serialization; -internal sealed class SessionMessageJsonConverter : JsonConverter +internal sealed class SessionMessageInfoJsonConverter : JsonConverter { private static readonly Dictionary TypesByTag = new(StringComparer.Ordinal) { - ["agent-switched"] = typeof(SessionMessageAgentSwitched), + ["agent-switched"] = typeof(SessionMessageAgentSelected), ["assistant"] = typeof(SessionMessageAssistant), ["compaction"] = typeof(SessionMessageCompaction), - ["model-switched"] = typeof(SessionMessageModelSwitched), + ["location-switched"] = typeof(SessionMessageLocationSwitched), + ["model-switched"] = typeof(SessionMessageModelSelected), ["shell"] = typeof(SessionMessageShell), + ["skill"] = typeof(SessionMessageSkill), ["synthetic"] = typeof(SessionMessageSynthetic), ["system"] = typeof(SessionMessageSystem), ["user"] = typeof(SessionMessageUser) }; public override bool HandleNull => true; - public override SessionMessage Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionMessageInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { ArgumentNullException.ThrowIfNull(typeToConvert); ArgumentNullException.ThrowIfNull(options); if (reader.TokenType == JsonTokenType.Null) { - throw new JsonException("The SessionMessage payload cannot be null."); + throw new JsonException("The SessionMessageInfo payload cannot be null."); } using var document = JsonDocument.ParseValue(ref reader); var payload = document.RootElement; if (payload.ValueKind != JsonValueKind.Object) { - throw new JsonException("The SessionMessage payload must be a JSON object."); + throw new JsonException("The SessionMessageInfo payload must be a JSON object."); } if (!payload.TryGetProperty("type", out var markerElement)) { - throw new JsonException("The SessionMessage payload must contain 'type'."); + throw new JsonException("The SessionMessageInfo payload must contain 'type'."); } if (markerElement.ValueKind != JsonValueKind.String) @@ -47,28 +49,39 @@ public override SessionMessage Read(ref Utf8JsonReader reader, Type typeToConver throw new JsonException("The 'type' marker must be a string."); } - var marker = markerElement.GetString() ?? throw new JsonException("The 'type' marker cannot be null."); + var marker = markerElement.GetString(); + if (marker is null || string.IsNullOrWhiteSpace(marker)) + { + throw new JsonException("The 'type' marker must be a non-empty string."); + } + if (TypesByTag.TryGetValue(marker, out var targetType)) { - var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for SessionMessage."); - return JsonSerializer.Deserialize(payload, typeInfo) as SessionMessage ?? throw new JsonException("The SessionMessage payload deserialized to null."); + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for SessionMessageInfo."); + return JsonSerializer.Deserialize(payload, typeInfo) as SessionMessageInfo ?? throw new JsonException("The SessionMessageInfo payload deserialized to null."); } - return new UnknownSessionMessage(marker, payload); + return new UnknownSessionMessageInfo(marker, payload); } - public override void Write(Utf8JsonWriter writer, SessionMessage value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SessionMessageInfo value, JsonSerializerOptions options) { ArgumentNullException.ThrowIfNull(writer); ArgumentNullException.ThrowIfNull(value); ArgumentNullException.ThrowIfNull(options); - if (value is UnknownSessionMessage unknown) + if (value is UnknownSessionMessageInfo unknown) { unknown.Payload.WriteTo(writer); return; } - var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(value.GetType()) ?? throw new JsonException("The generated context has no metadata for SessionMessage."); + if (value is SessionMessageCompaction nestedSessionMessageCompaction) + { + JsonSerializer.Serialize(writer, nestedSessionMessageCompaction, OpenCodeJsonContext.Default.SessionMessageCompaction); + return; + } + + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(value.GetType()) ?? throw new JsonException("The generated context has no metadata for SessionMessageInfo."); JsonSerializer.Serialize(writer, value, typeInfo); } } diff --git a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageToolStateJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageToolStateJsonConverter.cs index a7c3431ca..cba657d11 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageToolStateJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageToolStateJsonConverter.cs @@ -12,8 +12,8 @@ internal sealed class SessionMessageToolStateJsonConverter : JsonConverter true; @@ -43,7 +43,12 @@ public override SessionMessageToolState Read(ref Utf8JsonReader reader, Type typ throw new JsonException("The 'status' marker must be a string."); } - var marker = markerElement.GetString() ?? throw new JsonException("The 'status' marker cannot be null."); + var marker = markerElement.GetString(); + if (marker is null || string.IsNullOrWhiteSpace(marker)) + { + throw new JsonException("The 'status' marker must be a non-empty string."); + } + if (TypesByTag.TryGetValue(marker, out var targetType)) { var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for SessionMessageToolState."); diff --git a/src/OpenCode.Sdk/Internal/Serialization/LlmToolContentJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/ToolContentJsonConverter.cs similarity index 64% rename from src/OpenCode.Sdk/Internal/Serialization/LlmToolContentJsonConverter.cs rename to src/OpenCode.Sdk/Internal/Serialization/ToolContentJsonConverter.cs index 0927249d1..8edbe1d7a 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/LlmToolContentJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/ToolContentJsonConverter.cs @@ -6,7 +6,7 @@ namespace OpenCode.Sdk.Internal.Serialization; -internal sealed class LlmToolContentJsonConverter : JsonConverter +internal sealed class ToolContentJsonConverter : JsonConverter { private static readonly Dictionary TypesByTag = new(StringComparer.Ordinal) { @@ -15,25 +15,25 @@ internal sealed class LlmToolContentJsonConverter : JsonConverter true; - public override LlmToolContent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ToolContent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { ArgumentNullException.ThrowIfNull(typeToConvert); ArgumentNullException.ThrowIfNull(options); if (reader.TokenType == JsonTokenType.Null) { - throw new JsonException("The LlmToolContent payload cannot be null."); + throw new JsonException("The ToolContent payload cannot be null."); } using var document = JsonDocument.ParseValue(ref reader); var payload = document.RootElement; if (payload.ValueKind != JsonValueKind.Object) { - throw new JsonException("The LlmToolContent payload must be a JSON object."); + throw new JsonException("The ToolContent payload must be a JSON object."); } if (!payload.TryGetProperty("type", out var markerElement)) { - throw new JsonException("The LlmToolContent payload must contain 'type'."); + throw new JsonException("The ToolContent payload must contain 'type'."); } if (markerElement.ValueKind != JsonValueKind.String) @@ -41,28 +41,33 @@ public override LlmToolContent Read(ref Utf8JsonReader reader, Type typeToConver throw new JsonException("The 'type' marker must be a string."); } - var marker = markerElement.GetString() ?? throw new JsonException("The 'type' marker cannot be null."); + var marker = markerElement.GetString(); + if (marker is null || string.IsNullOrWhiteSpace(marker)) + { + throw new JsonException("The 'type' marker must be a non-empty string."); + } + if (TypesByTag.TryGetValue(marker, out var targetType)) { - var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for LlmToolContent."); - return JsonSerializer.Deserialize(payload, typeInfo) as LlmToolContent ?? throw new JsonException("The LlmToolContent payload deserialized to null."); + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for ToolContent."); + return JsonSerializer.Deserialize(payload, typeInfo) as ToolContent ?? throw new JsonException("The ToolContent payload deserialized to null."); } - return new UnknownLlmToolContent(marker, payload); + return new UnknownToolContent(marker, payload); } - public override void Write(Utf8JsonWriter writer, LlmToolContent value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ToolContent value, JsonSerializerOptions options) { ArgumentNullException.ThrowIfNull(writer); ArgumentNullException.ThrowIfNull(value); ArgumentNullException.ThrowIfNull(options); - if (value is UnknownLlmToolContent unknown) + if (value is UnknownToolContent unknown) { unknown.Payload.WriteTo(writer); return; } - var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(value.GetType()) ?? throw new JsonException("The generated context has no metadata for LlmToolContent."); + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(value.GetType()) ?? throw new JsonException("The generated context has no metadata for ToolContent."); JsonSerializer.Serialize(writer, value, typeInfo); } } diff --git a/src/OpenCode.Sdk/Internal/SystemEnvironmentProvider.cs b/src/OpenCode.Sdk/Internal/SystemEnvironmentProvider.cs new file mode 100644 index 000000000..c74b5b1cb --- /dev/null +++ b/src/OpenCode.Sdk/Internal/SystemEnvironmentProvider.cs @@ -0,0 +1,14 @@ +using OpenCode.Sdk.Internal.Abstractions; + +namespace OpenCode.Sdk.Internal; + +/// Reads environment variables from the current process. +internal sealed class SystemEnvironmentProvider : IEnvironmentProvider +{ + public string? GetEnvironmentVariable(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + + return Environment.GetEnvironmentVariable(name); + } +} diff --git a/src/OpenCode.Sdk/Internal/UserAgentPolicy.cs b/src/OpenCode.Sdk/Internal/UserAgentPolicy.cs new file mode 100644 index 000000000..178906f88 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/UserAgentPolicy.cs @@ -0,0 +1,31 @@ +using System.Net.Http.Headers; +using System.Reflection; + +namespace OpenCode.Sdk.Internal; + +/// Composes the SDK's User-Agent product token. +internal static class UserAgentPolicy +{ + private const string ProductName = "OpenCode.Sdk"; + + /// Reads the SDK assembly's informational version into the product token. + public static ProductInfoHeaderValue Resolve() => + Compose(typeof(UserAgentPolicy).Assembly + .GetCustomAttribute()?.InformationalVersion); + + /// + /// Composes the product token from an informational version. Build metadata after + /// + is stripped; a missing or unparsable version omits the version token + /// entirely — never a silent substitute, never a construction failure + /// (maintainer, 2026-08-13). + /// + /// The assembly's informational version, when present. + /// The product token for User-Agent decoration. + public static ProductInfoHeaderValue Compose(string? informationalVersion) + { + var version = informationalVersion?.Split('+')[0].Trim(); + return !string.IsNullOrEmpty(version) && ProductHeaderValue.TryParse($"{ProductName}/{version}", out var parsed) + ? new ProductInfoHeaderValue(parsed) + : new ProductInfoHeaderValue(new ProductHeaderValue(ProductName)); + } +} diff --git a/src/OpenCode.Sdk/Models/LocationRef.cs b/src/OpenCode.Sdk/Models/LocationRef.cs new file mode 100644 index 000000000..3b753731e --- /dev/null +++ b/src/OpenCode.Sdk/Models/LocationRef.cs @@ -0,0 +1,23 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a location ref value. +/// +public sealed record LocationRef +{ + /// + /// Gets the directory value. + /// + [JsonPropertyName("directory")] + public required string Directory { get; init; } + + /// + /// Gets the workspace id value. + /// + [JsonPropertyName("workspaceID")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? WorkspaceId { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/MessageNotFoundError.cs b/src/OpenCode.Sdk/Models/MessageNotFoundError.cs index 4ee91c6b3..20449fcfa 100644 --- a/src/OpenCode.Sdk/Models/MessageNotFoundError.cs +++ b/src/OpenCode.Sdk/Models/MessageNotFoundError.cs @@ -18,13 +18,13 @@ public sealed record MessageNotFoundError : OpenCodeError /// Gets the session id value. /// [JsonPropertyName("sessionID")] - public required string SessionID { get; init; } + public required string SessionId { get; init; } /// /// Gets the message id value. /// [JsonPropertyName("messageID")] - public required string MessageID { get; init; } + public required string MessageId { get; init; } /// /// Gets the message value. diff --git a/src/OpenCode.Sdk/Models/ModelRef.cs b/src/OpenCode.Sdk/Models/ModelRef.cs index 6b7bb6179..4c3cd5232 100644 --- a/src/OpenCode.Sdk/Models/ModelRef.cs +++ b/src/OpenCode.Sdk/Models/ModelRef.cs @@ -12,13 +12,13 @@ public sealed record ModelRef /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the provider id value. /// [JsonPropertyName("providerID")] - public required string ProviderID { get; init; } + public required string ProviderId { get; init; } /// /// Gets the variant value. diff --git a/src/OpenCode.Sdk/Models/PromptAgentAttachment.cs b/src/OpenCode.Sdk/Models/PromptAgentAttachment.cs index f56e39235..7541d366c 100644 --- a/src/OpenCode.Sdk/Models/PromptAgentAttachment.cs +++ b/src/OpenCode.Sdk/Models/PromptAgentAttachment.cs @@ -15,9 +15,9 @@ public sealed record PromptAgentAttachment public required string Name { get; init; } /// - /// Gets the source value. + /// Gets the mention value. /// - [JsonPropertyName("source")] + [JsonPropertyName("mention")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public PromptSource? Source { get; init; } + public PromptMention? Mention { get; init; } } diff --git a/src/OpenCode.Sdk/Models/PromptFileAttachment.cs b/src/OpenCode.Sdk/Models/PromptFileAttachment.cs index f17bd3416..1bc0abf68 100644 --- a/src/OpenCode.Sdk/Models/PromptFileAttachment.cs +++ b/src/OpenCode.Sdk/Models/PromptFileAttachment.cs @@ -9,10 +9,10 @@ namespace OpenCode.Sdk.Models; public sealed record PromptFileAttachment { /// - /// Gets the uri value. + /// Gets the data value. /// - [JsonPropertyName("uri")] - public required Uri Uri { get; init; } + [JsonPropertyName("data")] + public required string Data { get; init; } /// /// Gets the mime value. @@ -20,6 +20,12 @@ public sealed record PromptFileAttachment [JsonPropertyName("mime")] public required string Mime { get; init; } + /// + /// Gets the source value. + /// + [JsonPropertyName("source")] + public required PromptFileSource Source { get; init; } + /// /// Gets the name value. /// @@ -35,9 +41,9 @@ public sealed record PromptFileAttachment public string? Description { get; init; } /// - /// Gets the source value. + /// Gets the mention value. /// - [JsonPropertyName("source")] + [JsonPropertyName("mention")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public PromptSource? Source { get; init; } + public PromptMention? Mention { get; init; } } diff --git a/src/OpenCode.Sdk/Models/LlmToolContent.cs b/src/OpenCode.Sdk/Models/PromptFileSource.cs similarity index 76% rename from src/OpenCode.Sdk/Models/LlmToolContent.cs rename to src/OpenCode.Sdk/Models/PromptFileSource.cs index 7be23e7b7..acd5526bd 100644 --- a/src/OpenCode.Sdk/Models/LlmToolContent.cs +++ b/src/OpenCode.Sdk/Models/PromptFileSource.cs @@ -5,10 +5,10 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a llm tool content union. +/// Represents a prompt file source union. /// -[JsonConverter(typeof(LlmToolContentJsonConverter))] -public abstract record LlmToolContent +[JsonConverter(typeof(PromptFileSourceJsonConverter))] +public abstract record PromptFileSource { /// /// Gets the 'type' union marker. diff --git a/src/OpenCode.Sdk/Models/PromptFileSourceInline.cs b/src/OpenCode.Sdk/Models/PromptFileSourceInline.cs new file mode 100644 index 000000000..30ecaa857 --- /dev/null +++ b/src/OpenCode.Sdk/Models/PromptFileSourceInline.cs @@ -0,0 +1,16 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a prompt file source inline value. +/// +public sealed record PromptFileSourceInline : PromptFileSource +{ + /// + /// Gets the type value. + /// + [JsonPropertyName("type")] + public override string Type => "inline"; +} diff --git a/src/OpenCode.Sdk/Models/PromptFileSourceUri.cs b/src/OpenCode.Sdk/Models/PromptFileSourceUri.cs new file mode 100644 index 000000000..dfb1816ec --- /dev/null +++ b/src/OpenCode.Sdk/Models/PromptFileSourceUri.cs @@ -0,0 +1,22 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a prompt file source uri value. +/// +public sealed record PromptFileSourceUri : PromptFileSource +{ + /// + /// Gets the type value. + /// + [JsonPropertyName("type")] + public override string Type => "uri"; + + /// + /// Gets the uri value. + /// + [JsonPropertyName("uri")] + public required Uri Uri { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/PromptSource.cs b/src/OpenCode.Sdk/Models/PromptMention.cs similarity index 90% rename from src/OpenCode.Sdk/Models/PromptSource.cs rename to src/OpenCode.Sdk/Models/PromptMention.cs index d1998bcc6..d2e13861a 100644 --- a/src/OpenCode.Sdk/Models/PromptSource.cs +++ b/src/OpenCode.Sdk/Models/PromptMention.cs @@ -4,9 +4,9 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a prompt source value. +/// Represents a prompt mention value. /// -public sealed record PromptSource +public sealed record PromptMention { /// /// Gets the start value. diff --git a/src/OpenCode.Sdk/Models/PromptSkillAttachment.cs b/src/OpenCode.Sdk/Models/PromptSkillAttachment.cs new file mode 100644 index 000000000..d8eaf56d6 --- /dev/null +++ b/src/OpenCode.Sdk/Models/PromptSkillAttachment.cs @@ -0,0 +1,35 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a prompt skill attachment value. +/// +public sealed record PromptSkillAttachment +{ + /// + /// Gets the id value. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// Gets the name value. + /// + [JsonPropertyName("name")] + public required string Name { get; init; } + + /// + /// Gets the text value. + /// + [JsonPropertyName("text")] + public required string Text { get; init; } + + /// + /// Gets the mention value. + /// + [JsonPropertyName("mention")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public PromptMention? Mention { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/ServiceHealth.cs b/src/OpenCode.Sdk/Models/ServiceHealth.cs new file mode 100644 index 000000000..8acc98c42 --- /dev/null +++ b/src/OpenCode.Sdk/Models/ServiceHealth.cs @@ -0,0 +1,28 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a service health value. +/// +public sealed record ServiceHealth +{ + /// + /// Gets the healthy value. + /// + [JsonPropertyName("healthy")] + public required bool Healthy { get; init; } + + /// + /// Gets the version value. + /// + [JsonPropertyName("version")] + public required string Version { get; init; } + + /// + /// Gets the pid value. + /// + [JsonPropertyName("pid")] + public required long Pid { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageAgentSwitched.cs b/src/OpenCode.Sdk/Models/SessionMessageAgentSelected.cs similarity index 50% rename from src/OpenCode.Sdk/Models/SessionMessageAgentSwitched.cs rename to src/OpenCode.Sdk/Models/SessionMessageAgentSelected.cs index 092d3f0ab..e96712c26 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAgentSwitched.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAgentSelected.cs @@ -3,38 +3,31 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// -/// Represents a session message agent switched value. +/// Represents a session message agent selected value. /// -public sealed record SessionMessageAgentSwitched : SessionMessage +public sealed record SessionMessageAgentSelected : SessionMessageInfo { /// /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the metadata value. /// [JsonPropertyName("metadata")] - public IReadOnlyDictionary Metadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)) : new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// /// Gets the time value. /// [JsonPropertyName("time")] - public required SessionMessageAgentSwitchedTime Time { get; init; } + public required SessionMessageAgentSelectedTime Time { get; init; } /// /// Gets the type value. @@ -47,4 +40,11 @@ public IReadOnlyDictionary Metadata /// [JsonPropertyName("agent")] public required string Agent { get; init; } + + /// + /// Gets the previous value. + /// + [JsonPropertyName("previous")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Previous { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageModelSwitchedTime.cs b/src/OpenCode.Sdk/Models/SessionMessageAgentSelectedTime.cs similarity index 78% rename from src/OpenCode.Sdk/Models/SessionMessageModelSwitchedTime.cs rename to src/OpenCode.Sdk/Models/SessionMessageAgentSelectedTime.cs index d01ce5341..1ecc63944 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageModelSwitchedTime.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAgentSelectedTime.cs @@ -4,9 +4,9 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a session message model switched time value. +/// Represents a session message agent selected time value. /// -public sealed record SessionMessageModelSwitchedTime +public sealed record SessionMessageAgentSelectedTime { /// /// Gets the created value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistant.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistant.cs index d4a1b3381..71b81f857 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistant.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistant.cs @@ -3,32 +3,25 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// /// Represents a session message assistant value. /// -public sealed record SessionMessageAssistant : SessionMessage +public sealed record SessionMessageAssistant : SessionMessageInfo { /// /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the metadata value. /// [JsonPropertyName("metadata")] - public IReadOnlyDictionary Metadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)) : new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// /// Gets the time value. @@ -80,7 +73,7 @@ public required IReadOnlyList Content /// [JsonPropertyName("finish")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Finish { get; init; } + public SessionMessageAssistantFinish? Finish { get; init; } /// /// Gets the cost value. @@ -94,12 +87,19 @@ public required IReadOnlyList Content /// [JsonPropertyName("tokens")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public SessionMessageAssistantTokens? Tokens { get; init; } + public TokenUsageInfo? Tokens { get; init; } /// /// Gets the error value. /// [JsonPropertyName("error")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public SessionErrorUnknown? Error { get; init; } + public SessionStructuredError? Error { get; init; } + + /// + /// Gets the retry value. + /// + [JsonPropertyName("retry")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SessionMessageAssistantRetry? Retry { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantFinish.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantFinish.cs new file mode 100644 index 000000000..be884f91f --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantFinish.cs @@ -0,0 +1,42 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Defines the supported session message assistant finish values. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionMessageAssistantFinish +{ + /// + /// Represents the 'stop' wire value. + /// + [JsonStringEnumMemberName("stop")] + Stop, + /// + /// Represents the 'length' wire value. + /// + [JsonStringEnumMemberName("length")] + Length, + /// + /// Represents the 'tool-calls' wire value. + /// + [JsonStringEnumMemberName("tool-calls")] + ToolCalls, + /// + /// Represents the 'content-filter' wire value. + /// + [JsonStringEnumMemberName("content-filter")] + ContentFilter, + /// + /// Represents the 'error' wire value. + /// + [JsonStringEnumMemberName("error")] + Error, + /// + /// Represents the 'unknown' wire value. + /// + [JsonStringEnumMemberName("unknown")] + Unknown +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs index 31ee4287b..381c16df8 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// @@ -16,12 +17,6 @@ public sealed record SessionMessageAssistantReasoning : SessionMessageAssistantC [JsonPropertyName("type")] public override string Type => "reasoning"; - /// - /// Gets the id value. - /// - [JsonPropertyName("id")] - public required string ID { get; init; } - /// /// Gets the text value. /// @@ -29,18 +24,10 @@ public sealed record SessionMessageAssistantReasoning : SessionMessageAssistantC public required string Text { get; init; } /// - /// Gets the provider metadata value. + /// Gets the state value. /// - [JsonPropertyName("providerMetadata")] - public IReadOnlyDictionary> ProviderMetadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary>(new Dictionary>(StringComparer.Ordinal)) : new ReadOnlyDictionary>(value.ToDictionary(static pair => pair.Key, static pair => (IReadOnlyDictionary)new ReadOnlyDictionary(pair.Value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)), StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary>(new Dictionary>(StringComparer.Ordinal)); + [JsonPropertyName("state")] + public IReadOnlyDictionary State { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// /// Gets the time value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantRetry.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantRetry.cs new file mode 100644 index 000000000..0baa92150 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantRetry.cs @@ -0,0 +1,28 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message assistant retry value. +/// +public sealed record SessionMessageAssistantRetry +{ + /// + /// Gets the attempt value. + /// + [JsonPropertyName("attempt")] + public required long Attempt { get; init; } + + /// + /// Gets the at value. + /// + [JsonPropertyName("at")] + public required double At { get; init; } + + /// + /// Gets the error value. + /// + [JsonPropertyName("error")] + public required SessionStructuredError Error { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantSnapshot.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantSnapshot.cs index 139143386..dd8338325 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantSnapshot.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantSnapshot.cs @@ -1,6 +1,7 @@ // Generated by OpenCode.Sdk.Tools from spec/openapi.json. // Do not edit by hand — change tools/curation.json or the emitters, then regenerate. using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// @@ -26,13 +27,5 @@ public sealed record SessionMessageAssistantSnapshot /// Gets the files value. /// [JsonPropertyName("files")] - public IReadOnlyList Files - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? Array.Empty() : new List(value).AsReadOnly(); - } - } = []; + public IReadOnlyList Files { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).AsReadOnly()); } = []; } diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs index 491ce784c..ff4bcb939 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs @@ -1,6 +1,9 @@ // Generated by OpenCode.Sdk.Tools from spec/openapi.json. // Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Collections.ObjectModel; +using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// @@ -14,15 +17,15 @@ public sealed record SessionMessageAssistantText : SessionMessageAssistantConten [JsonPropertyName("type")] public override string Type => "text"; - /// - /// Gets the id value. - /// - [JsonPropertyName("id")] - public required string ID { get; init; } - /// /// Gets the text value. /// [JsonPropertyName("text")] public required string Text { get; init; } + + /// + /// Gets the state value. + /// + [JsonPropertyName("state")] + public IReadOnlyDictionary State { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); } diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs index 7013ccb2e..eb63d0b2e 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs @@ -1,6 +1,9 @@ // Generated by OpenCode.Sdk.Tools from spec/openapi.json. // Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Collections.ObjectModel; +using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// @@ -18,7 +21,7 @@ public sealed record SessionMessageAssistantTool : SessionMessageAssistantConten /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the name value. @@ -27,11 +30,23 @@ public sealed record SessionMessageAssistantTool : SessionMessageAssistantConten public required string Name { get; init; } /// - /// Gets the provider value. + /// Gets the executed value. /// - [JsonPropertyName("provider")] + [JsonPropertyName("executed")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public SessionMessageAssistantToolProvider? Provider { get; init; } + public bool? Executed { get; init; } + + /// + /// Gets the provider state value. + /// + [JsonPropertyName("providerState")] + public IReadOnlyDictionary ProviderState { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + + /// + /// Gets the provider result state value. + /// + [JsonPropertyName("providerResultState")] + public IReadOnlyDictionary ProviderResultState { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// /// Gets the state value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantToolProvider.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantToolProvider.cs deleted file mode 100644 index 44f442297..000000000 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantToolProvider.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Generated by OpenCode.Sdk.Tools from spec/openapi.json. -// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using System.Collections.ObjectModel; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace OpenCode.Sdk.Models; -/// -/// Represents a session message assistant tool provider value. -/// -public sealed record SessionMessageAssistantToolProvider -{ - /// - /// Gets the executed value. - /// - [JsonPropertyName("executed")] - public required bool Executed { get; init; } - - /// - /// Gets the metadata value. - /// - [JsonPropertyName("metadata")] - public IReadOnlyDictionary> Metadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary>(new Dictionary>(StringComparer.Ordinal)) : new ReadOnlyDictionary>(value.ToDictionary(static pair => pair.Key, static pair => (IReadOnlyDictionary)new ReadOnlyDictionary(pair.Value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)), StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary>(new Dictionary>(StringComparer.Ordinal)); - - /// - /// Gets the result metadata value. - /// - [JsonPropertyName("resultMetadata")] - public IReadOnlyDictionary> ResultMetadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary>(new Dictionary>(StringComparer.Ordinal)) : new ReadOnlyDictionary>(value.ToDictionary(static pair => pair.Key, static pair => (IReadOnlyDictionary)new ReadOnlyDictionary(pair.Value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)), StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary>(new Dictionary>(StringComparer.Ordinal)); -} diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantToolTime.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantToolTime.cs index 12a3894ea..966de6236 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantToolTime.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantToolTime.cs @@ -27,11 +27,4 @@ public sealed record SessionMessageAssistantToolTime [JsonPropertyName("completed")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? Completed { get; init; } - - /// - /// Gets the pruned value. - /// - [JsonPropertyName("pruned")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public double? Pruned { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompaction.cs b/src/OpenCode.Sdk/Models/SessionMessageCompaction.cs index de7ba3c01..58bdeb5c2 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageCompaction.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageCompaction.cs @@ -1,62 +1,18 @@ // Generated by OpenCode.Sdk.Tools from spec/openapi.json. // Do not edit by hand — change tools/curation.json or the emitters, then regenerate. -using System.Collections.ObjectModel; -using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// -/// Represents a session message compaction value. +/// Represents a session message compaction union. /// -public sealed record SessionMessageCompaction : SessionMessage +[JsonConverter(typeof(SessionMessageCompactionJsonConverter))] +public abstract record SessionMessageCompaction : SessionMessageInfo { /// - /// Gets the type value. + /// Gets the 'status' union marker. /// - [JsonPropertyName("type")] - public override string Type => "compaction"; - - /// - /// Gets the reason value. - /// - [JsonPropertyName("reason")] - public required SessionMessageCompactionReason Reason { get; init; } - - /// - /// Gets the summary value. - /// - [JsonPropertyName("summary")] - public required string Summary { get; init; } - - /// - /// Gets the recent value. - /// - [JsonPropertyName("recent")] - public required string Recent { get; init; } - - /// - /// Gets the id value. - /// - [JsonPropertyName("id")] - public required string ID { get; init; } - - /// - /// Gets the metadata value. - /// - [JsonPropertyName("metadata")] - public IReadOnlyDictionary Metadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)) : new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); - - /// - /// Gets the time value. - /// - [JsonPropertyName("time")] - public required SessionMessageCompactionTime Time { get; init; } + [JsonPropertyName("status")] + public abstract string Status { get; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs new file mode 100644 index 000000000..1b60f7585 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs @@ -0,0 +1,61 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message compaction completed value. +/// +public sealed record SessionMessageCompactionCompleted : SessionMessageCompaction +{ + /// + /// Gets the type value. + /// + [JsonPropertyName("type")] + public override string Type => "compaction"; + + /// + /// Gets the id value. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// Gets the metadata value. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + + /// + /// Gets the time value. + /// + [JsonPropertyName("time")] + public required SessionMessageCompactionCompletedTime Time { get; init; } + + /// + /// Gets the status value. + /// + [JsonPropertyName("status")] + public override string Status => "completed"; + + /// + /// Gets the reason value. + /// + [JsonPropertyName("reason")] + public required SessionMessageCompactionCompletedReason Reason { get; init; } + + /// + /// Gets the summary value. + /// + [JsonPropertyName("summary")] + public required string Summary { get; init; } + + /// + /// Gets the recent value. + /// + [JsonPropertyName("recent")] + public required string Recent { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionCompletedReason.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompletedReason.cs new file mode 100644 index 000000000..c35159b21 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompletedReason.cs @@ -0,0 +1,22 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Defines the supported session message compaction completed reason values. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionMessageCompactionCompletedReason +{ + /// + /// Represents the 'auto' wire value. + /// + [JsonStringEnumMemberName("auto")] + Auto, + /// + /// Represents the 'manual' wire value. + /// + [JsonStringEnumMemberName("manual")] + Manual +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionCompletedTime.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompletedTime.cs new file mode 100644 index 000000000..a7ff48c97 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompletedTime.cs @@ -0,0 +1,16 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message compaction completed time value. +/// +public sealed record SessionMessageCompactionCompletedTime +{ + /// + /// Gets the created value. + /// + [JsonPropertyName("created")] + public required double Created { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionFailed.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionFailed.cs new file mode 100644 index 000000000..1d92e93cd --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionFailed.cs @@ -0,0 +1,55 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message compaction failed value. +/// +public sealed record SessionMessageCompactionFailed : SessionMessageCompaction +{ + /// + /// Gets the type value. + /// + [JsonPropertyName("type")] + public override string Type => "compaction"; + + /// + /// Gets the id value. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// Gets the metadata value. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + + /// + /// Gets the time value. + /// + [JsonPropertyName("time")] + public required SessionMessageCompactionFailedTime Time { get; init; } + + /// + /// Gets the status value. + /// + [JsonPropertyName("status")] + public override string Status => "failed"; + + /// + /// Gets the reason value. + /// + [JsonPropertyName("reason")] + public required SessionMessageCompactionFailedReason Reason { get; init; } + + /// + /// Gets the error value. + /// + [JsonPropertyName("error")] + public required SessionStructuredError Error { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionReason.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionFailedReason.cs similarity index 80% rename from src/OpenCode.Sdk/Models/SessionMessageCompactionReason.cs rename to src/OpenCode.Sdk/Models/SessionMessageCompactionFailedReason.cs index e5b0238ed..78fa17a9a 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageCompactionReason.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionFailedReason.cs @@ -4,10 +4,10 @@ namespace OpenCode.Sdk.Models; /// -/// Defines the supported session message compaction reason values. +/// Defines the supported session message compaction failed reason values. /// -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum SessionMessageCompactionReason +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionMessageCompactionFailedReason { /// /// Represents the 'auto' wire value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionFailedTime.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionFailedTime.cs new file mode 100644 index 000000000..c044c1c38 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionFailedTime.cs @@ -0,0 +1,16 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message compaction failed time value. +/// +public sealed record SessionMessageCompactionFailedTime +{ + /// + /// Gets the created value. + /// + [JsonPropertyName("created")] + public required double Created { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionRunning.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionRunning.cs new file mode 100644 index 000000000..5f5c91393 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionRunning.cs @@ -0,0 +1,61 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message compaction running value. +/// +public sealed record SessionMessageCompactionRunning : SessionMessageCompaction +{ + /// + /// Gets the type value. + /// + [JsonPropertyName("type")] + public override string Type => "compaction"; + + /// + /// Gets the id value. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// Gets the metadata value. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + + /// + /// Gets the time value. + /// + [JsonPropertyName("time")] + public required SessionMessageCompactionRunningTime Time { get; init; } + + /// + /// Gets the status value. + /// + [JsonPropertyName("status")] + public override string Status => "running"; + + /// + /// Gets the reason value. + /// + [JsonPropertyName("reason")] + public required SessionMessageCompactionRunningReason Reason { get; init; } + + /// + /// Gets the summary value. + /// + [JsonPropertyName("summary")] + public required string Summary { get; init; } + + /// + /// Gets the recent value. + /// + [JsonPropertyName("recent")] + public required string Recent { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionRunningReason.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionRunningReason.cs new file mode 100644 index 000000000..d214ed1ff --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionRunningReason.cs @@ -0,0 +1,22 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Defines the supported session message compaction running reason values. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionMessageCompactionRunningReason +{ + /// + /// Represents the 'auto' wire value. + /// + [JsonStringEnumMemberName("auto")] + Auto, + /// + /// Represents the 'manual' wire value. + /// + [JsonStringEnumMemberName("manual")] + Manual +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionRunningTime.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionRunningTime.cs new file mode 100644 index 000000000..81680c56d --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionRunningTime.cs @@ -0,0 +1,16 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message compaction running time value. +/// +public sealed record SessionMessageCompactionRunningTime +{ + /// + /// Gets the created value. + /// + [JsonPropertyName("created")] + public required double Created { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageInfo.cs b/src/OpenCode.Sdk/Models/SessionMessageInfo.cs new file mode 100644 index 000000000..7a21a455c --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageInfo.cs @@ -0,0 +1,18 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message info union. +/// +[JsonConverter(typeof(SessionMessageInfoJsonConverter))] +public abstract record SessionMessageInfo +{ + /// + /// Gets the 'type' union marker. + /// + [JsonPropertyName("type")] + public abstract string Type { get; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageLocationSwitched.cs b/src/OpenCode.Sdk/Models/SessionMessageLocationSwitched.cs new file mode 100644 index 000000000..368dd3835 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageLocationSwitched.cs @@ -0,0 +1,64 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message location switched value. +/// +public sealed record SessionMessageLocationSwitched : SessionMessageInfo +{ + /// + /// Gets the id value. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// Gets the metadata value. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + + /// + /// Gets the time value. + /// + [JsonPropertyName("time")] + public required SessionMessageLocationSwitchedTime Time { get; init; } + + /// + /// Gets the type value. + /// + [JsonPropertyName("type")] + public override string Type => "location-switched"; + + /// + /// Gets the location value. + /// + [JsonPropertyName("location")] + public required LocationRef Location { get; init; } + + /// + /// Gets the project id value. + /// + [JsonPropertyName("projectID")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ProjectId { get; init; } + + /// + /// Gets the subpath value. + /// + [JsonPropertyName("subpath")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Subpath { get; init; } + + /// + /// Gets the previous value. + /// + [JsonPropertyName("previous")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SessionMessageLocationSwitchedPrevious? Previous { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageLocationSwitchedPrevious.cs b/src/OpenCode.Sdk/Models/SessionMessageLocationSwitchedPrevious.cs new file mode 100644 index 000000000..c50418897 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageLocationSwitchedPrevious.cs @@ -0,0 +1,30 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message location switched previous value. +/// +public sealed record SessionMessageLocationSwitchedPrevious +{ + /// + /// Gets the location value. + /// + [JsonPropertyName("location")] + public required LocationRef Location { get; init; } + + /// + /// Gets the project id value. + /// + [JsonPropertyName("projectID")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ProjectId { get; init; } + + /// + /// Gets the subpath value. + /// + [JsonPropertyName("subpath")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Subpath { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageLocationSwitchedTime.cs b/src/OpenCode.Sdk/Models/SessionMessageLocationSwitchedTime.cs new file mode 100644 index 000000000..9d72d7205 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageLocationSwitchedTime.cs @@ -0,0 +1,16 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message location switched time value. +/// +public sealed record SessionMessageLocationSwitchedTime +{ + /// + /// Gets the created value. + /// + [JsonPropertyName("created")] + public required double Created { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageModelSwitched.cs b/src/OpenCode.Sdk/Models/SessionMessageModelSelected.cs similarity index 50% rename from src/OpenCode.Sdk/Models/SessionMessageModelSwitched.cs rename to src/OpenCode.Sdk/Models/SessionMessageModelSelected.cs index b40383fdc..72e598d8e 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageModelSwitched.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageModelSelected.cs @@ -3,38 +3,31 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// -/// Represents a session message model switched value. +/// Represents a session message model selected value. /// -public sealed record SessionMessageModelSwitched : SessionMessage +public sealed record SessionMessageModelSelected : SessionMessageInfo { /// /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the metadata value. /// [JsonPropertyName("metadata")] - public IReadOnlyDictionary Metadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)) : new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// /// Gets the time value. /// [JsonPropertyName("time")] - public required SessionMessageModelSwitchedTime Time { get; init; } + public required SessionMessageModelSelectedTime Time { get; init; } /// /// Gets the type value. @@ -47,4 +40,11 @@ public IReadOnlyDictionary Metadata /// [JsonPropertyName("model")] public required ModelRef Model { get; init; } + + /// + /// Gets the previous value. + /// + [JsonPropertyName("previous")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ModelRef? Previous { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageAgentSwitchedTime.cs b/src/OpenCode.Sdk/Models/SessionMessageModelSelectedTime.cs similarity index 78% rename from src/OpenCode.Sdk/Models/SessionMessageAgentSwitchedTime.cs rename to src/OpenCode.Sdk/Models/SessionMessageModelSelectedTime.cs index 0d4f7dd17..ae0f65368 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAgentSwitchedTime.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageModelSelectedTime.cs @@ -4,9 +4,9 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a session message agent switched time value. +/// Represents a session message model selected time value. /// -public sealed record SessionMessageAgentSwitchedTime +public sealed record SessionMessageModelSelectedTime { /// /// Gets the created value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageShell.cs b/src/OpenCode.Sdk/Models/SessionMessageShell.cs index d382d0874..ba204ead0 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageShell.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageShell.cs @@ -3,32 +3,25 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// /// Represents a session message shell value. /// -public sealed record SessionMessageShell : SessionMessage +public sealed record SessionMessageShell : SessionMessageInfo { /// /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the metadata value. /// [JsonPropertyName("metadata")] - public IReadOnlyDictionary Metadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)) : new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// /// Gets the time value. @@ -43,10 +36,10 @@ public IReadOnlyDictionary Metadata public override string Type => "shell"; /// - /// Gets the call id value. + /// Gets the shell id value. /// - [JsonPropertyName("callID")] - public required string CallID { get; init; } + [JsonPropertyName("shellID")] + public required string ShellId { get; init; } /// /// Gets the command value. @@ -54,9 +47,23 @@ public IReadOnlyDictionary Metadata [JsonPropertyName("command")] public required string Command { get; init; } + /// + /// Gets the status value. + /// + [JsonPropertyName("status")] + public required SessionMessageShellStatus Status { get; init; } + + /// + /// Gets the exit value. + /// + [JsonPropertyName("exit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Exit { get; init; } + /// /// Gets the output value. /// [JsonPropertyName("output")] - public required string Output { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SessionMessageShellOutput? Output { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageShellOutput.cs b/src/OpenCode.Sdk/Models/SessionMessageShellOutput.cs new file mode 100644 index 000000000..1ba1b4f3f --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageShellOutput.cs @@ -0,0 +1,34 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message shell output value. +/// +public sealed record SessionMessageShellOutput +{ + /// + /// Gets the output value. + /// + [JsonPropertyName("output")] + public required string Output { get; init; } + + /// + /// Gets the cursor value. + /// + [JsonPropertyName("cursor")] + public required long Cursor { get; init; } + + /// + /// Gets the size value. + /// + [JsonPropertyName("size")] + public required long Size { get; init; } + + /// + /// Gets the truncated value. + /// + [JsonPropertyName("truncated")] + public required bool Truncated { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageShellStatus.cs b/src/OpenCode.Sdk/Models/SessionMessageShellStatus.cs new file mode 100644 index 000000000..0db35d57a --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageShellStatus.cs @@ -0,0 +1,32 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Defines the supported session message shell status values. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionMessageShellStatus +{ + /// + /// Represents the 'running' wire value. + /// + [JsonStringEnumMemberName("running")] + Running, + /// + /// Represents the 'exited' wire value. + /// + [JsonStringEnumMemberName("exited")] + Exited, + /// + /// Represents the 'timeout' wire value. + /// + [JsonStringEnumMemberName("timeout")] + Timeout, + /// + /// Represents the 'killed' wire value. + /// + [JsonStringEnumMemberName("killed")] + Killed +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageSkill.cs b/src/OpenCode.Sdk/Models/SessionMessageSkill.cs new file mode 100644 index 000000000..c16b10843 --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionMessageSkill.cs @@ -0,0 +1,55 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session message skill value. +/// +public sealed record SessionMessageSkill : SessionMessageInfo +{ + /// + /// Gets the id value. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// Gets the metadata value. + /// + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + + /// + /// Gets the time value. + /// + [JsonPropertyName("time")] + public required SessionMessageSkillTime Time { get; init; } + + /// + /// Gets the type value. + /// + [JsonPropertyName("type")] + public override string Type => "skill"; + + /// + /// Gets the skill value. + /// + [JsonPropertyName("skill")] + public required string Skill { get; init; } + + /// + /// Gets the name value. + /// + [JsonPropertyName("name")] + public required string Name { get; init; } + + /// + /// Gets the text value. + /// + [JsonPropertyName("text")] + public required string Text { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionTime.cs b/src/OpenCode.Sdk/Models/SessionMessageSkillTime.cs similarity index 79% rename from src/OpenCode.Sdk/Models/SessionMessageCompactionTime.cs rename to src/OpenCode.Sdk/Models/SessionMessageSkillTime.cs index e4ee8680e..24d56c382 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageCompactionTime.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageSkillTime.cs @@ -4,9 +4,9 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a session message compaction time value. +/// Represents a session message skill time value. /// -public sealed record SessionMessageCompactionTime +public sealed record SessionMessageSkillTime { /// /// Gets the created value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageSynthetic.cs b/src/OpenCode.Sdk/Models/SessionMessageSynthetic.cs index b617eb925..0a5157ff1 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageSynthetic.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageSynthetic.cs @@ -3,32 +3,25 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// /// Represents a session message synthetic value. /// -public sealed record SessionMessageSynthetic : SessionMessage +public sealed record SessionMessageSynthetic : SessionMessageInfo { /// /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the metadata value. /// [JsonPropertyName("metadata")] - public IReadOnlyDictionary Metadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)) : new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// /// Gets the time value. @@ -36,18 +29,19 @@ public IReadOnlyDictionary Metadata [JsonPropertyName("time")] public required SessionMessageSyntheticTime Time { get; init; } - /// - /// Gets the session id value. - /// - [JsonPropertyName("sessionID")] - public required string SessionID { get; init; } - /// /// Gets the text value. /// [JsonPropertyName("text")] public required string Text { get; init; } + /// + /// Gets the description value. + /// + [JsonPropertyName("description")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Description { get; init; } + /// /// Gets the type value. /// diff --git a/src/OpenCode.Sdk/Models/SessionMessageSystem.cs b/src/OpenCode.Sdk/Models/SessionMessageSystem.cs index b15265d99..9a8a53cf1 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageSystem.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageSystem.cs @@ -3,32 +3,25 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// /// Represents a session message system value. /// -public sealed record SessionMessageSystem : SessionMessage +public sealed record SessionMessageSystem : SessionMessageInfo { /// /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the metadata value. /// [JsonPropertyName("metadata")] - public IReadOnlyDictionary Metadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)) : new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// /// Gets the time value. @@ -47,4 +40,11 @@ public IReadOnlyDictionary Metadata /// [JsonPropertyName("text")] public required string Text { get; init; } + + /// + /// Gets the description value. + /// + [JsonPropertyName("description")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Description { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageToolStateCompleted.cs b/src/OpenCode.Sdk/Models/SessionMessageToolStateCompleted.cs index c7fcfa054..2de1a1ff5 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageToolStateCompleted.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageToolStateCompleted.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// @@ -30,66 +31,23 @@ public required IReadOnlyDictionary Input } } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); - /// - /// Gets the attachments value. - /// - [JsonPropertyName("attachments")] - public IReadOnlyList Attachments - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? Array.Empty() : new List(value).AsReadOnly(); - } - } = []; - /// /// Gets the content value. /// [JsonPropertyName("content")] - public required IReadOnlyList Content + public required IReadOnlyList Content { get; init { ArgumentNullException.ThrowIfNull(value); - field = new List(value).AsReadOnly(); + field = new List(value).AsReadOnly(); } } = []; /// - /// Gets the output paths value. - /// - [JsonPropertyName("outputPaths")] - public IReadOnlyList OutputPaths - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? Array.Empty() : new List(value).AsReadOnly(); - } - } = []; - - /// - /// Gets the structured value. - /// - [JsonPropertyName("structured")] - public required IReadOnlyDictionary Structured - { - get; - init - { - ArgumentNullException.ThrowIfNull(value); - field = new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); - - /// - /// Gets the result value. + /// Gets the metadata value. /// - [JsonPropertyName("result")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonElement? Result { get; init; } + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); } diff --git a/src/OpenCode.Sdk/Models/SessionMessageToolStateError.cs b/src/OpenCode.Sdk/Models/SessionMessageToolStateError.cs index 4500937a7..a155107da 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageToolStateError.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageToolStateError.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// @@ -31,43 +32,20 @@ public required IReadOnlyDictionary Input } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// - /// Gets the content value. - /// - [JsonPropertyName("content")] - public required IReadOnlyList Content - { - get; - init - { - ArgumentNullException.ThrowIfNull(value); - field = new List(value).AsReadOnly(); - } - } = []; - - /// - /// Gets the structured value. + /// Gets the error value. /// - [JsonPropertyName("structured")] - public required IReadOnlyDictionary Structured - { - get; - init - { - ArgumentNullException.ThrowIfNull(value); - field = new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + [JsonPropertyName("error")] + public required SessionStructuredError Error { get; init; } /// - /// Gets the error value. + /// Gets the content value. /// - [JsonPropertyName("error")] - public required SessionErrorUnknown Error { get; init; } + [JsonPropertyName("content")] + public IReadOnlyList Content { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).AsReadOnly()); } = []; /// - /// Gets the result value. + /// Gets the metadata value. /// - [JsonPropertyName("result")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonElement? Result { get; init; } + [JsonPropertyName("metadata")] + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); } diff --git a/src/OpenCode.Sdk/Models/SessionMessageToolStateRunning.cs b/src/OpenCode.Sdk/Models/SessionMessageToolStateRunning.cs index 983eb02fa..35138add6 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageToolStateRunning.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageToolStateRunning.cs @@ -31,10 +31,10 @@ public required IReadOnlyDictionary Input } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// - /// Gets the structured value. + /// Gets the metadata value. /// - [JsonPropertyName("structured")] - public required IReadOnlyDictionary Structured + [JsonPropertyName("metadata")] + public required IReadOnlyDictionary Metadata { get; init @@ -43,18 +43,4 @@ public required IReadOnlyDictionary Structured field = new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); } } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); - - /// - /// Gets the content value. - /// - [JsonPropertyName("content")] - public required IReadOnlyList Content - { - get; - init - { - ArgumentNullException.ThrowIfNull(value); - field = new List(value).AsReadOnly(); - } - } = []; } diff --git a/src/OpenCode.Sdk/Models/SessionMessageToolStatePending.cs b/src/OpenCode.Sdk/Models/SessionMessageToolStateStreaming.cs similarity index 72% rename from src/OpenCode.Sdk/Models/SessionMessageToolStatePending.cs rename to src/OpenCode.Sdk/Models/SessionMessageToolStateStreaming.cs index 6c89ec192..2336a8f50 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageToolStatePending.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageToolStateStreaming.cs @@ -4,15 +4,15 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a session message tool state pending value. +/// Represents a session message tool state streaming value. /// -public sealed record SessionMessageToolStatePending : SessionMessageToolState +public sealed record SessionMessageToolStateStreaming : SessionMessageToolState { /// /// Gets the status value. /// [JsonPropertyName("status")] - public override string Status => "pending"; + public override string Status => "streaming"; /// /// Gets the input value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageUser.cs b/src/OpenCode.Sdk/Models/SessionMessageUser.cs index 4987e920a..8636a7ef5 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageUser.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageUser.cs @@ -3,32 +3,25 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// /// Represents a session message user value. /// -public sealed record SessionMessageUser : SessionMessage +public sealed record SessionMessageUser : SessionMessageInfo { /// /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the metadata value. /// [JsonPropertyName("metadata")] - public IReadOnlyDictionary Metadata - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)) : new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + public IReadOnlyDictionary Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); /// /// Gets the time value. @@ -46,29 +39,19 @@ public IReadOnlyDictionary Metadata /// Gets the files value. /// [JsonPropertyName("files")] - public IReadOnlyList Files - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? Array.Empty() : new List(value).AsReadOnly(); - } - } = []; + public IReadOnlyList Files { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).AsReadOnly()); } = []; /// /// Gets the agents value. /// [JsonPropertyName("agents")] - public IReadOnlyList Agents - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? Array.Empty() : new List(value).AsReadOnly(); - } - } = []; + public IReadOnlyList Agents { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).AsReadOnly()); } = []; + + /// + /// Gets the skills value. + /// + [JsonPropertyName("skills")] + public IReadOnlyList Skills { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).AsReadOnly()); } = []; /// /// Gets the type value. diff --git a/src/OpenCode.Sdk/Models/SessionNotFoundError.cs b/src/OpenCode.Sdk/Models/SessionNotFoundError.cs index 092b396bf..ff3483159 100644 --- a/src/OpenCode.Sdk/Models/SessionNotFoundError.cs +++ b/src/OpenCode.Sdk/Models/SessionNotFoundError.cs @@ -18,7 +18,7 @@ public sealed record SessionNotFoundError : OpenCodeError /// Gets the session id value. /// [JsonPropertyName("sessionID")] - public required string SessionID { get; init; } + public required string SessionId { get; init; } /// /// Gets the message value. diff --git a/src/OpenCode.Sdk/Models/SessionErrorUnknown.cs b/src/OpenCode.Sdk/Models/SessionStructuredError.cs similarity index 64% rename from src/OpenCode.Sdk/Models/SessionErrorUnknown.cs rename to src/OpenCode.Sdk/Models/SessionStructuredError.cs index ff316f61c..fa9a32aeb 100644 --- a/src/OpenCode.Sdk/Models/SessionErrorUnknown.cs +++ b/src/OpenCode.Sdk/Models/SessionStructuredError.cs @@ -4,9 +4,9 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a session error unknown value. +/// Represents a session structured error value. /// -public sealed record SessionErrorUnknown +public sealed record SessionStructuredError { /// /// Gets the type value. @@ -19,4 +19,11 @@ public sealed record SessionErrorUnknown /// [JsonPropertyName("message")] public required string Message { get; init; } + + /// + /// Gets the status value. + /// + [JsonPropertyName("status")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public long? Status { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantTokens.cs b/src/OpenCode.Sdk/Models/TokenUsageInfo.cs similarity index 81% rename from src/OpenCode.Sdk/Models/SessionMessageAssistantTokens.cs rename to src/OpenCode.Sdk/Models/TokenUsageInfo.cs index f324769eb..2dc6f638e 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantTokens.cs +++ b/src/OpenCode.Sdk/Models/TokenUsageInfo.cs @@ -4,9 +4,9 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a session message assistant tokens value. +/// Represents a token usage info value. /// -public sealed record SessionMessageAssistantTokens +public sealed record TokenUsageInfo { /// /// Gets the input value. @@ -30,5 +30,5 @@ public sealed record SessionMessageAssistantTokens /// Gets the cache value. /// [JsonPropertyName("cache")] - public required SessionMessageAssistantTokensCache Cache { get; init; } + public required TokenUsageInfoCache Cache { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantTokensCache.cs b/src/OpenCode.Sdk/Models/TokenUsageInfoCache.cs similarity index 81% rename from src/OpenCode.Sdk/Models/SessionMessageAssistantTokensCache.cs rename to src/OpenCode.Sdk/Models/TokenUsageInfoCache.cs index 078ed48af..21b1e1a81 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantTokensCache.cs +++ b/src/OpenCode.Sdk/Models/TokenUsageInfoCache.cs @@ -4,9 +4,9 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a session message assistant tokens cache value. +/// Represents a token usage info cache value. /// -public sealed record SessionMessageAssistantTokensCache +public sealed record TokenUsageInfoCache { /// /// Gets the read value. diff --git a/src/OpenCode.Sdk/Models/SessionMessage.cs b/src/OpenCode.Sdk/Models/ToolContent.cs similarity index 77% rename from src/OpenCode.Sdk/Models/SessionMessage.cs rename to src/OpenCode.Sdk/Models/ToolContent.cs index 256ca83be..6c679353f 100644 --- a/src/OpenCode.Sdk/Models/SessionMessage.cs +++ b/src/OpenCode.Sdk/Models/ToolContent.cs @@ -5,10 +5,10 @@ namespace OpenCode.Sdk.Models; /// -/// Represents a session message union. +/// Represents a tool content union. /// -[JsonConverter(typeof(SessionMessageJsonConverter))] -public abstract record SessionMessage +[JsonConverter(typeof(ToolContentJsonConverter))] +public abstract record ToolContent { /// /// Gets the 'type' union marker. diff --git a/src/OpenCode.Sdk/Models/ToolFileContent.cs b/src/OpenCode.Sdk/Models/ToolFileContent.cs index 698ab9671..cf65dbe60 100644 --- a/src/OpenCode.Sdk/Models/ToolFileContent.cs +++ b/src/OpenCode.Sdk/Models/ToolFileContent.cs @@ -6,7 +6,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a tool file content value. /// -public sealed record ToolFileContent : LlmToolContent +public sealed record ToolFileContent : ToolContent { /// /// Gets the type value. diff --git a/src/OpenCode.Sdk/Models/ToolTextContent.cs b/src/OpenCode.Sdk/Models/ToolTextContent.cs index 0e285c2a8..4ed67bbe2 100644 --- a/src/OpenCode.Sdk/Models/ToolTextContent.cs +++ b/src/OpenCode.Sdk/Models/ToolTextContent.cs @@ -6,7 +6,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a tool text content value. /// -public sealed record ToolTextContent : LlmToolContent +public sealed record ToolTextContent : ToolContent { /// /// Gets the type value. diff --git a/src/OpenCode.Sdk/Models/UnknownOpenCodeError.cs b/src/OpenCode.Sdk/Models/UnknownOpenCodeError.cs index fa84ba418..130eb3c17 100644 --- a/src/OpenCode.Sdk/Models/UnknownOpenCodeError.cs +++ b/src/OpenCode.Sdk/Models/UnknownOpenCodeError.cs @@ -24,6 +24,7 @@ public UnknownOpenCodeError(string tag, JsonElement payload) /// /// Gets the unrecognized '_tag' marker. /// + [JsonPropertyName("_tag")] public override string Tag => _marker; /// /// Gets the preserved raw JSON payload. diff --git a/src/OpenCode.Sdk/Models/UnknownSessionMessage.cs b/src/OpenCode.Sdk/Models/UnknownPromptFileSource.cs similarity index 79% rename from src/OpenCode.Sdk/Models/UnknownSessionMessage.cs rename to src/OpenCode.Sdk/Models/UnknownPromptFileSource.cs index 464f80cbd..adfee4732 100644 --- a/src/OpenCode.Sdk/Models/UnknownSessionMessage.cs +++ b/src/OpenCode.Sdk/Models/UnknownPromptFileSource.cs @@ -5,16 +5,16 @@ namespace OpenCode.Sdk.Models; /// -/// Preserves an unknown session message payload. +/// Preserves an unknown prompt file source payload. /// -public sealed record UnknownSessionMessage : SessionMessage +public sealed record UnknownPromptFileSource : PromptFileSource { private readonly string _marker; /// /// Initializes an unknown union value from its marker and raw payload. /// [JsonConstructor] - public UnknownSessionMessage(string type, JsonElement payload) + public UnknownPromptFileSource(string type, JsonElement payload) { ArgumentException.ThrowIfNullOrEmpty(type); _marker = type; @@ -24,6 +24,7 @@ public UnknownSessionMessage(string type, JsonElement payload) /// /// Gets the unrecognized 'type' marker. /// + [JsonPropertyName("type")] public override string Type => _marker; /// /// Gets the preserved raw JSON payload. diff --git a/src/OpenCode.Sdk/Models/UnknownSessionMessageAssistantContent.cs b/src/OpenCode.Sdk/Models/UnknownSessionMessageAssistantContent.cs index c903cb1ab..143c2b238 100644 --- a/src/OpenCode.Sdk/Models/UnknownSessionMessageAssistantContent.cs +++ b/src/OpenCode.Sdk/Models/UnknownSessionMessageAssistantContent.cs @@ -24,6 +24,7 @@ public UnknownSessionMessageAssistantContent(string type, JsonElement payload) /// /// Gets the unrecognized 'type' marker. /// + [JsonPropertyName("type")] public override string Type => _marker; /// /// Gets the preserved raw JSON payload. diff --git a/src/OpenCode.Sdk/Models/UnknownSessionMessageCompaction.cs b/src/OpenCode.Sdk/Models/UnknownSessionMessageCompaction.cs new file mode 100644 index 000000000..96268f855 --- /dev/null +++ b/src/OpenCode.Sdk/Models/UnknownSessionMessageCompaction.cs @@ -0,0 +1,39 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Preserves an unknown session message compaction payload. +/// +public sealed record UnknownSessionMessageCompaction : SessionMessageCompaction +{ + private readonly string _marker; + /// + /// Initializes an unknown union value from its marker and raw payload. + /// + [JsonConstructor] + public UnknownSessionMessageCompaction(string status, JsonElement payload) + { + ArgumentException.ThrowIfNullOrEmpty(status); + _marker = status; + Payload = payload.Clone(); + } + + /// + /// Gets the unrecognized 'status' marker. + /// + [JsonPropertyName("status")] + public override string Status => _marker; + + /// + /// Gets the fixed 'type' marker of this nested union. + /// + [JsonPropertyName("type")] + public override string Type => "compaction"; + /// + /// Gets the preserved raw JSON payload. + /// + public JsonElement Payload { get; } +} diff --git a/src/OpenCode.Sdk/Models/UnknownSessionMessageInfo.cs b/src/OpenCode.Sdk/Models/UnknownSessionMessageInfo.cs new file mode 100644 index 000000000..c77f46428 --- /dev/null +++ b/src/OpenCode.Sdk/Models/UnknownSessionMessageInfo.cs @@ -0,0 +1,33 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Preserves an unknown session message info payload. +/// +public sealed record UnknownSessionMessageInfo : SessionMessageInfo +{ + private readonly string _marker; + /// + /// Initializes an unknown union value from its marker and raw payload. + /// + [JsonConstructor] + public UnknownSessionMessageInfo(string type, JsonElement payload) + { + ArgumentException.ThrowIfNullOrEmpty(type); + _marker = type; + Payload = payload.Clone(); + } + + /// + /// Gets the unrecognized 'type' marker. + /// + [JsonPropertyName("type")] + public override string Type => _marker; + /// + /// Gets the preserved raw JSON payload. + /// + public JsonElement Payload { get; } +} diff --git a/src/OpenCode.Sdk/Models/UnknownSessionMessageToolState.cs b/src/OpenCode.Sdk/Models/UnknownSessionMessageToolState.cs index 547ff3670..7c1eac2f9 100644 --- a/src/OpenCode.Sdk/Models/UnknownSessionMessageToolState.cs +++ b/src/OpenCode.Sdk/Models/UnknownSessionMessageToolState.cs @@ -24,6 +24,7 @@ public UnknownSessionMessageToolState(string status, JsonElement payload) /// /// Gets the unrecognized 'status' marker. /// + [JsonPropertyName("status")] public override string Status => _marker; /// /// Gets the preserved raw JSON payload. diff --git a/src/OpenCode.Sdk/Models/UnknownLlmToolContent.cs b/src/OpenCode.Sdk/Models/UnknownToolContent.cs similarity index 80% rename from src/OpenCode.Sdk/Models/UnknownLlmToolContent.cs rename to src/OpenCode.Sdk/Models/UnknownToolContent.cs index 58b819840..e961cbf6e 100644 --- a/src/OpenCode.Sdk/Models/UnknownLlmToolContent.cs +++ b/src/OpenCode.Sdk/Models/UnknownToolContent.cs @@ -5,16 +5,16 @@ namespace OpenCode.Sdk.Models; /// -/// Preserves an unknown llm tool content payload. +/// Preserves an unknown tool content payload. /// -public sealed record UnknownLlmToolContent : LlmToolContent +public sealed record UnknownToolContent : ToolContent { private readonly string _marker; /// /// Initializes an unknown union value from its marker and raw payload. /// [JsonConstructor] - public UnknownLlmToolContent(string type, JsonElement payload) + public UnknownToolContent(string type, JsonElement payload) { ArgumentException.ThrowIfNullOrEmpty(type); _marker = type; @@ -24,6 +24,7 @@ public UnknownLlmToolContent(string type, JsonElement payload) /// /// Gets the unrecognized 'type' marker. /// + [JsonPropertyName("type")] public override string Type => _marker; /// /// Gets the preserved raw JSON payload. diff --git a/src/OpenCode.Sdk/OpenCode.Sdk.csproj b/src/OpenCode.Sdk/OpenCode.Sdk.csproj index bfc3d1bed..5734826f3 100644 --- a/src/OpenCode.Sdk/OpenCode.Sdk.csproj +++ b/src/OpenCode.Sdk/OpenCode.Sdk.csproj @@ -5,6 +5,8 @@ Unofficial .NET SDK for opencode: a typed client for the opencode HTTP API. opencode;sdk;client;ai;agent true + + true true @@ -13,4 +15,15 @@ + + + + + + + + + + + diff --git a/src/OpenCode.Sdk/OpenCodeApiException.cs b/src/OpenCode.Sdk/OpenCodeApiException.cs new file mode 100644 index 000000000..6511eb3c5 --- /dev/null +++ b/src/OpenCode.Sdk/OpenCodeApiException.cs @@ -0,0 +1,49 @@ +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk; + +/// Represents a non-success response returned by the opencode API. +public class OpenCodeApiException : OpenCodeException +{ + /// Initializes a new instance of the class. + public OpenCodeApiException() + { + } + + /// Initializes a new instance of the class. + /// The failure description. + public OpenCodeApiException(string message) + : base(message) + { + } + + /// Initializes a new instance of the class. + /// The failure description. + /// The underlying failure. + public OpenCodeApiException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// Initializes a new instance of the class. + /// The failure description. + /// The HTTP status code of the error response. + /// The typed API error, when the error body carried a known shape. + /// The raw error body. + public OpenCodeApiException(string message, int status, OpenCodeError? error, string? rawBody) + : base(message) + { + Status = status; + Error = error; + RawBody = rawBody; + } + + /// Gets the HTTP status code of the error response. + public int Status { get; } + + /// Gets the typed API error, or when the error body could not be parsed. + public OpenCodeError? Error { get; } + + /// Gets the raw error body. + public string? RawBody { get; } +} diff --git a/src/OpenCode.Sdk/OpenCodeClient.cs b/src/OpenCode.Sdk/OpenCodeClient.cs new file mode 100644 index 000000000..90fc48d1e --- /dev/null +++ b/src/OpenCode.Sdk/OpenCodeClient.cs @@ -0,0 +1,90 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using OpenCode.Sdk.Internal; +using OpenCode.Sdk.Internal.ResponseAdapters; + +namespace OpenCode.Sdk; +/// +/// The opencode API client and composition root. +/// +public class OpenCodeClient : IDisposable +{ + private readonly Pipeline? _pipeline; + private readonly SessionsClient? _sessions; + /// + /// Initializes a client that owns its connection to the endpoint. + /// + /// The absolute HTTP or HTTPS server endpoint. + public OpenCodeClient(Uri endpoint) : this(endpoint, options: null) + { + } + + /// + /// Initializes a client that owns its connection to the endpoint. + /// + /// The absolute HTTP or HTTPS server endpoint. + /// The client options; its endpoint must stay unset on this path. + public OpenCodeClient(Uri endpoint, OpenCodeClientOptions? options) + { + _pipeline = Pipeline.Create(endpoint, options, new SystemEnvironmentProvider()); + _sessions = new SessionsClient(_pipeline); + } + + /// + /// Initializes a client over a caller-owned HttpClient; the SDK never disposes it. + /// + /// The caller-owned HTTP client. + /// The client options; the endpoint is required on this path. + public OpenCodeClient(HttpClient httpClient, OpenCodeClientOptions options) + { + _pipeline = Pipeline.Create(httpClient, options, new SystemEnvironmentProvider()); + _sessions = new SessionsClient(_pipeline); + } + + /// + /// Initializes a mocking instance; members invoked without an override throw an instructive failure. + /// + protected OpenCodeClient() + { + } + + /// + /// Releases the owned transport resources. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the owned transport resources. + /// + /// Whether managed state should be released. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _pipeline?.Dispose(); + } + } + + /// + /// Gets the 'Sessions' collection client. + /// + public virtual SessionsClient Sessions => _sessions ?? throw MockSeam.CreateError("OpenCodeClient", "Sessions"); + private Pipeline Pipeline => _pipeline ?? throw MockSeam.CreateError("OpenCodeClient", "Pipeline"); + + /// + /// Check server health + /// + /// The per-call options. + /// The cancellation token. + /// The 'HealthResponse' envelope. + /// The API returned an error status (declared: 400, 401) and NoThrow was not selected. + /// The server could not be reached or returned a malformed success body. + public virtual Task GetHealthAsync(OpenCodeRequestOptions? options = null, CancellationToken cancellationToken = default) + { + return Pipeline.ExecuteAsync(HttpMethod.Get, OpenCodeRoutes.Health.Get, HealthResponseAdapter.Instance, options, cancellationToken); + } +} diff --git a/src/OpenCode.Sdk/OpenCodeClientOptions.cs b/src/OpenCode.Sdk/OpenCodeClientOptions.cs new file mode 100644 index 000000000..b6524f2d9 --- /dev/null +++ b/src/OpenCode.Sdk/OpenCodeClientOptions.cs @@ -0,0 +1,19 @@ +namespace OpenCode.Sdk; + +/// Configures an opencode client for its whole lifetime. +public sealed class OpenCodeClientOptions +{ + /// + /// Gets or sets the absolute HTTP or HTTPS server endpoint. Required when a caller + /// supplies its own HttpClient; must stay unset when a constructor receives + /// the endpoint directly. + /// + public Uri? Endpoint { get; set; } + + /// + /// Gets or sets the HTTP basic-authentication password for the opencode user. + /// When unset, the OPENCODE_SERVER_PASSWORD environment variable is read once at + /// client construction. + /// + public string? Password { get; set; } +} diff --git a/src/OpenCode.Sdk/OpenCodeException.cs b/src/OpenCode.Sdk/OpenCodeException.cs new file mode 100644 index 000000000..bcf375f82 --- /dev/null +++ b/src/OpenCode.Sdk/OpenCodeException.cs @@ -0,0 +1,25 @@ +namespace OpenCode.Sdk; + +/// Represents any failure raised by the opencode SDK. +public class OpenCodeException : Exception +{ + /// Initializes a new instance of the class. + public OpenCodeException() + { + } + + /// Initializes a new instance of the class. + /// The failure description. + public OpenCodeException(string message) + : base(message) + { + } + + /// Initializes a new instance of the class. + /// The failure description. + /// The underlying failure. + public OpenCodeException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/OpenCode.Sdk/OpenCodeRequestOptions.cs b/src/OpenCode.Sdk/OpenCodeRequestOptions.cs new file mode 100644 index 000000000..41e88a0be --- /dev/null +++ b/src/OpenCode.Sdk/OpenCodeRequestOptions.cs @@ -0,0 +1,14 @@ +namespace OpenCode.Sdk; + +/// Configures one opencode API call. +public sealed class OpenCodeRequestOptions +{ + /// Gets the error channel for this call. + public ErrorBehavior ErrorBehavior { get; init; } + + /// Gets a shared instance that returns API errors on the response envelope. + public static OpenCodeRequestOptions NoThrow { get; } = new() + { + ErrorBehavior = ErrorBehavior.NoThrow, + }; +} diff --git a/src/OpenCode.Sdk/OpenCodeResponse.cs b/src/OpenCode.Sdk/OpenCodeResponse.cs new file mode 100644 index 000000000..8cd085ec9 --- /dev/null +++ b/src/OpenCode.Sdk/OpenCodeResponse.cs @@ -0,0 +1,19 @@ +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk; + +/// Carries the shared metadata of every typed opencode response envelope. +public abstract record OpenCodeResponse +{ + /// Gets the HTTP status code of the response. + public required int Status { get; init; } + + /// Gets a value indicating whether the response is an API error. + public bool IsError { get; init; } + + /// Gets the typed API error, or when the error body could not be parsed. + public OpenCodeError? Error { get; init; } + + /// Gets the raw error body; populated only on the error path. + public string? RawBody { get; init; } +} diff --git a/src/OpenCode.Sdk/OpenCodeRoutes.cs b/src/OpenCode.Sdk/OpenCodeRoutes.cs new file mode 100644 index 000000000..10863cba6 --- /dev/null +++ b/src/OpenCode.Sdk/OpenCodeRoutes.cs @@ -0,0 +1,52 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +namespace OpenCode.Sdk; +/// +/// Defines the wire route of every generated operation. +/// +public static class OpenCodeRoutes +{ + /// + /// Defines the 'Health' routes. + /// + public static class Health + { + /// + /// The 'GET /api/health' route. + /// + public const string Get = "/api/health"; + } + + /// + /// Defines the 'Sessions' routes. + /// + public static class Sessions + { + /// + /// The 'GET /api/session/{sessionID}/message/{messageID}' route template. + /// + public const string GetMessageTemplate = "/api/session/{sessionID}/message/{messageID}"; + /// + /// Builds the '/api/session/{sessionID}/message/{messageID}' route. + /// + /// The 'sessionID' route value. + /// The 'messageID' route value. + /// The escaped route. + public static string GetMessage(string sessionId, string messageId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(sessionId); + if (sessionId is "." or "..") + { + throw new ArgumentException("Route values must not be dot segments.", nameof(sessionId)); + } + + ArgumentException.ThrowIfNullOrWhiteSpace(messageId); + if (messageId is "." or "..") + { + throw new ArgumentException("Route values must not be dot segments.", nameof(messageId)); + } + + return "/api/session/" + Uri.EscapeDataString(sessionId) + "/message/" + Uri.EscapeDataString(messageId); + } + } +} diff --git a/src/OpenCode.Sdk/OpenCodeTransportException.cs b/src/OpenCode.Sdk/OpenCodeTransportException.cs new file mode 100644 index 000000000..8f9168535 --- /dev/null +++ b/src/OpenCode.Sdk/OpenCodeTransportException.cs @@ -0,0 +1,25 @@ +namespace OpenCode.Sdk; + +/// Represents a network or protocol failure below the opencode API contract. +public class OpenCodeTransportException : OpenCodeException +{ + /// Initializes a new instance of the class. + public OpenCodeTransportException() + { + } + + /// Initializes a new instance of the class. + /// The failure description. + public OpenCodeTransportException(string message) + : base(message) + { + } + + /// Initializes a new instance of the class. + /// The failure description. + /// The underlying failure. + public OpenCodeTransportException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/OpenCode.Sdk/Properties/AssemblyInfo.cs b/src/OpenCode.Sdk/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..e9c07fea0 --- /dev/null +++ b/src/OpenCode.Sdk/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("OpenCode.Sdk.Tests")] + +// Benchmarks measure the production serialization dispatch, not a reflection stand-in. +[assembly: InternalsVisibleTo("OpenCode.Sdk.Performance.Tests")] + +// NSubstitute proxies internal seam interfaces through Castle DynamicProxy. +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/OpenCode.Sdk/SessionClient.cs b/src/OpenCode.Sdk/SessionClient.cs new file mode 100644 index 000000000..0a148ca63 --- /dev/null +++ b/src/OpenCode.Sdk/SessionClient.cs @@ -0,0 +1,46 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using OpenCode.Sdk.Internal; +using OpenCode.Sdk.Internal.ResponseAdapters; + +namespace OpenCode.Sdk; +/// +/// A bound 'SessionClient' handle; it holds an immutable identifier and the shared pipeline. +/// +public class SessionClient +{ + private readonly Pipeline? _pipeline; + private readonly string? _sessionId; + internal SessionClient(Pipeline pipeline, string sessionId) + { + ArgumentNullException.ThrowIfNull(pipeline); + ArgumentException.ThrowIfNullOrEmpty(sessionId); + _pipeline = pipeline; + _sessionId = sessionId; + } + + /// + /// Initializes a mocking instance; members invoked without an override throw an instructive failure. + /// + protected SessionClient() + { + } + + private Pipeline Pipeline => _pipeline ?? throw MockSeam.CreateError("SessionClient", "Pipeline"); + private string SessionId => _sessionId ?? throw MockSeam.CreateError("SessionClient", "SessionId"); + + /// + /// Get session message + /// + /// The 'messageID' route value. + /// The per-call options. + /// The cancellation token. + /// The 'SessionMessageResponse' envelope. + /// The API returned an error status (declared: 400, 401, 404) and NoThrow was not selected. + /// The server could not be reached or returned a malformed success body. + public virtual Task GetMessageAsync(string messageId, OpenCodeRequestOptions? options = null, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(messageId); + return Pipeline.ExecuteAsync(HttpMethod.Get, OpenCodeRoutes.Sessions.GetMessage(SessionId, messageId), SessionMessageResponseAdapter.Instance, options, cancellationToken); + } +} diff --git a/src/OpenCode.Sdk/SessionMessageResponse.cs b/src/OpenCode.Sdk/SessionMessageResponse.cs new file mode 100644 index 000000000..aa3824d3b --- /dev/null +++ b/src/OpenCode.Sdk/SessionMessageResponse.cs @@ -0,0 +1,59 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Diagnostics.CodeAnalysis; +using System.Text; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk; +/// +/// Represents the response of the 'GET /api/session/{sessionID}/message/{messageID}' operation. +/// +public sealed record SessionMessageResponse : OpenCodeResponse +{ + private readonly SessionMessageInfo? _message; + /// + /// Initializes a success instance of the 'SessionMessageResponse' envelope. + /// + public SessionMessageResponse() + { + } + + /// + /// Initializes an error-path instance; the payload stays unset behind its guard. + /// + [SetsRequiredMembers] + internal SessionMessageResponse(int status, OpenCodeError? error, string? rawBody) + { + Status = status; + IsError = true; + Error = error; + RawBody = rawBody; + Message = null!; + } + + /// + /// Gets the Message payload; guarded on the error path. + /// + public required SessionMessageInfo Message { get => _message ?? throw new InvalidOperationException("The response is an error; check IsError before accessing Message."); init => _message = value; } + + /// + /// Prints the shared metadata and appends the payload only when it is present. + /// + protected override bool PrintMembers(StringBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + var printed = base.PrintMembers(builder); + if (_message is null) + { + return printed; + } + + if (printed) + { + _ = builder.Append(", "); + } + + _ = builder.Append("Message = ").Append(_message); + return true; + } +} diff --git a/src/OpenCode.Sdk/SessionsClient.cs b/src/OpenCode.Sdk/SessionsClient.cs new file mode 100644 index 000000000..d24afb4e8 --- /dev/null +++ b/src/OpenCode.Sdk/SessionsClient.cs @@ -0,0 +1,37 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using OpenCode.Sdk.Internal; + +namespace OpenCode.Sdk; +/// +/// The 'SessionsClient' collection client. +/// +public class SessionsClient +{ + private readonly Pipeline? _pipeline; + internal SessionsClient(Pipeline pipeline) + { + ArgumentNullException.ThrowIfNull(pipeline); + _pipeline = pipeline; + } + + /// + /// Initializes a mocking instance; members invoked without an override throw an instructive failure. + /// + protected SessionsClient() + { + } + + /// + /// Gets a bound 'SessionClient'; the handle never caches server state. + /// + /// The 'sessionID' route value. + /// The bound 'SessionClient'. + public virtual SessionClient GetSessionClient(string sessionId) + { + ArgumentException.ThrowIfNullOrEmpty(sessionId); + return new SessionClient(Pipeline, sessionId); + } + + private Pipeline Pipeline => _pipeline ?? throw MockSeam.CreateError("SessionsClient", "Pipeline"); +} diff --git a/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/ClientOperationBenchmarks.cs b/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/ClientOperationBenchmarks.cs new file mode 100644 index 000000000..ed04b3436 --- /dev/null +++ b/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/ClientOperationBenchmarks.cs @@ -0,0 +1,72 @@ +using BenchmarkDotNet.Attributes; +using OpenCode.Sdk.Performance.Tests.Support; + +namespace OpenCode.Sdk.Performance.Tests.Benchmarks; + +/// +/// End-to-end operation cost through the real pipeline over a canned in-memory response: +/// request build, auth decoration, buffering, adapter, and the full deserialization chain. +/// Baselines for the envelope single-pass (P2), success-path buffering (P3), and the +/// converter walk they feed (P1); issues #18 and #23. +/// +[MemoryDiagnoser] +public class ClientOperationBenchmarks : IDisposable +{ + private CannedResponseHandler? _messageHandler; + private CannedResponseHandler? _healthHandler; + private HttpClient? _messageHttpClient; + private HttpClient? _healthHttpClient; + private OpenCodeClient? _messageClient; + private OpenCodeClient? _healthClient; + private SessionClient? _session; + + [GlobalSetup] + public async Task SetupAsync() + { + var options = new OpenCodeClientOptions + { + Endpoint = new Uri("https://benchmark.invalid"), + Password = "benchmark", + }; + + var envelope = BenchmarkFixtures.DataEnvelope(await BenchmarkFixtures.DeepAssistantMessageAsync().ConfigureAwait(false)); + _messageHandler = new CannedResponseHandler(envelope); + _messageHttpClient = new HttpClient(_messageHandler); + _messageClient = new OpenCodeClient(_messageHttpClient, options); + _session = _messageClient.Sessions.GetSessionClient("ses_bench0000000000000000001"); + + _healthHandler = new CannedResponseHandler(BenchmarkFixtures.HealthBody()); + _healthHttpClient = new HttpClient(_healthHandler); + _healthClient = new OpenCodeClient(_healthHttpClient, options); + } + + [Benchmark] + public Task GetMessageAsync() => _session!.GetMessageAsync("msg_bench00000000000000000001"); + + [Benchmark] + public Task GetHealthAsync() => _healthClient!.GetHealthAsync(); + + [GlobalCleanup] + public void Cleanup() => Dispose(); + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _messageClient?.Dispose(); + _healthClient?.Dispose(); + _messageHttpClient?.Dispose(); + _healthHttpClient?.Dispose(); + _messageHandler?.Dispose(); + _healthHandler?.Dispose(); + } +} diff --git a/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/UnionDeserializationBenchmarks.cs b/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/UnionDeserializationBenchmarks.cs new file mode 100644 index 000000000..2221606d3 --- /dev/null +++ b/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/UnionDeserializationBenchmarks.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using BenchmarkDotNet.Attributes; +using OpenCode.Sdk.Internal.Serialization; +using OpenCode.Sdk.Models; +using OpenCode.Sdk.Performance.Tests.Support; + +namespace OpenCode.Sdk.Performance.Tests.Benchmarks; + +/// +/// The nested union converter walk in isolation (message → assistant content → tool state → +/// tool content), through the same source-generated dispatch the SDK uses. Baseline for the +/// single-pass converter redesign (issue #23, P1/P5). +/// +[MemoryDiagnoser] +public class UnionDeserializationBenchmarks +{ + private byte[] _deepAssistantMessage = []; + + [GlobalSetup] + public async Task SetupAsync() => _deepAssistantMessage = await BenchmarkFixtures.DeepAssistantMessageAsync().ConfigureAwait(false); + + [Benchmark] + public SessionMessageInfo? DeserializeDeepAssistantMessage() + => JsonSerializer.Deserialize(_deepAssistantMessage, OpenCodeJsonContext.Default.SessionMessageInfo); +} diff --git a/tests/OpenCode.Sdk.Performance.Tests/Fixtures/deep-assistant-message.json b/tests/OpenCode.Sdk.Performance.Tests/Fixtures/deep-assistant-message.json new file mode 100644 index 000000000..a15ed7e27 --- /dev/null +++ b/tests/OpenCode.Sdk.Performance.Tests/Fixtures/deep-assistant-message.json @@ -0,0 +1,64 @@ +{ + "id": "msg_bench00000000000000000001", + "type": "assistant", + "agent": "build", + "model": { "id": "bench-model-large", "providerID": "bench", "variant": "max" }, + "time": { "created": 1755100000, "completed": 1755100042 }, + "cost": 0.4231, + "finish": "stop", + "tokens": { + "input": 12403, + "output": 2211, + "reasoning": 3120, + "cache": { "read": 9500, "write": 1200 } + }, + "content": [ + { + "type": "reasoning", + "text": "The user wants the repository's failing integration test isolated before any fix lands. First reproduce the failure deterministically, then bisect the fixture data to the smallest payload that still triggers the defect, and only then look at the adapter boundary where the exception surfaces.", + "time": { "created": 1755100001, "completed": 1755100004 } + }, + { + "type": "text", + "text": "I reproduced the failure locally and narrowed it down to the response adapter: the buffered body is re-parsed once per union level, and the innermost tool payload is where the malformed marker surfaces. The fix belongs in the converter dispatch, not in the adapter. Below is the run that demonstrates the failure and the minimal fixture that triggers it.", + "state": {} + }, + { + "type": "tool", + "id": "tool_bench_shell_1", + "name": "bash", + "time": { "created": 1755100005, "ran": 1755100006, "completed": 1755100011 }, + "providerState": { "sandbox": true }, + "providerResultState": { "exitClass": "ok" }, + "state": { + "status": "completed", + "input": { "command": "dotnet test --configuration Release --filter Category=Serialization" }, + "metadata": { "exitCode": 0, "durationMs": 5211 }, + "content": [ + { + "type": "text", + "text": "Test run for OpenCode.Sdk.Tests.dll (net10.0)\nPassed! - Failed: 0, Passed: 214, Skipped: 0, Total: 214, Duration: 4.9s\nThe serialization suite is green after the converter dispatch change; the previously failing deep-union fixture now round-trips." + }, + { + "type": "file", + "mime": "text/plain", + "uri": "https://bench.invalid/artifacts/test-run-output.txt", + "name": "test-run-output.txt" + } + ] + } + }, + { + "type": "tool", + "id": "tool_bench_fetch_2", + "name": "webfetch", + "time": { "created": 1755100012 }, + "state": { + "status": "running", + "input": { "url": "https://bench.invalid/docs/union-dispatch", "prompt": "summarize the dispatch contract" }, + "metadata": { "attempt": 1 } + } + } + ], + "metadata": { "benchmark": true, "scenario": "deep-union-walk" } +} diff --git a/tests/OpenCode.Sdk.Performance.Tests/OpenCode.Sdk.Performance.Tests.csproj b/tests/OpenCode.Sdk.Performance.Tests/OpenCode.Sdk.Performance.Tests.csproj new file mode 100644 index 000000000..9399e5974 --- /dev/null +++ b/tests/OpenCode.Sdk.Performance.Tests/OpenCode.Sdk.Performance.Tests.csproj @@ -0,0 +1,26 @@ + + + + Exe + $(DefaultTargetFramework) + + true + + false + + $(NoWarn);S1075;S2068 + + + + + + + + + + + + + + + diff --git a/tests/OpenCode.Sdk.Performance.Tests/Program.cs b/tests/OpenCode.Sdk.Performance.Tests/Program.cs new file mode 100644 index 000000000..631f43b7c --- /dev/null +++ b/tests/OpenCode.Sdk.Performance.Tests/Program.cs @@ -0,0 +1,8 @@ +using BenchmarkDotNet.Running; + +namespace OpenCode.Sdk.Performance.Tests; + +internal static class Program +{ + private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); +} diff --git a/tests/OpenCode.Sdk.Performance.Tests/Support/BenchmarkFixtures.cs b/tests/OpenCode.Sdk.Performance.Tests/Support/BenchmarkFixtures.cs new file mode 100644 index 000000000..ff15e8c0b --- /dev/null +++ b/tests/OpenCode.Sdk.Performance.Tests/Support/BenchmarkFixtures.cs @@ -0,0 +1,31 @@ +namespace OpenCode.Sdk.Performance.Tests.Support; + +/// Loads the canned wire payloads the benchmarks replay. +internal static class BenchmarkFixtures +{ + /// A deep assistant message: reasoning + text + two tool parts across four union levels. + public static Task DeepAssistantMessageAsync() => ReadAsync("deep-assistant-message.json"); + + /// The bare health payload the live server returns. + public static byte[] HealthBody() => "{\"healthy\":true,\"version\":\"0.0.0-bench\",\"pid\":42}"u8.ToArray(); + + /// Wraps a payload in the {"data": ...} success envelope. + public static byte[] DataEnvelope(byte[] payload) + { + ArgumentNullException.ThrowIfNull(payload); + return [.. "{\"data\":"u8, .. payload, .. "}"u8]; + } + + private static async Task ReadAsync(string name) + { + var stream = typeof(BenchmarkFixtures).Assembly + .GetManifestResourceStream($"OpenCode.Sdk.Performance.Tests.Fixtures.{name}") + ?? throw new InvalidOperationException($"Embedded fixture '{name}' is missing."); + await using (stream.ConfigureAwait(false)) + { + await using var buffer = new MemoryStream(); + await stream.CopyToAsync(buffer).ConfigureAwait(false); + return buffer.ToArray(); + } + } +} diff --git a/tests/OpenCode.Sdk.Performance.Tests/Support/CannedResponseHandler.cs b/tests/OpenCode.Sdk.Performance.Tests/Support/CannedResponseHandler.cs new file mode 100644 index 000000000..cbf5e7a02 --- /dev/null +++ b/tests/OpenCode.Sdk.Performance.Tests/Support/CannedResponseHandler.cs @@ -0,0 +1,19 @@ +using System.Net; +using System.Net.Http.Headers; + +namespace OpenCode.Sdk.Performance.Tests.Support; + +/// +/// Answers every request with the same 200 JSON body, so benchmarks measure the SDK pipeline +/// and serialization only — no network, no server. The per-call response/content allocations +/// are part of every measurement equally and cancel out in comparisons. +/// +internal sealed class CannedResponseHandler(byte[] body) : HttpMessageHandler +{ + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var content = new ByteArrayContent(body); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = content }); + } +} diff --git a/tests/OpenCode.Sdk.Sandbox/OpenCode.Sdk.Sandbox.csproj b/tests/OpenCode.Sdk.Sandbox/OpenCode.Sdk.Sandbox.csproj new file mode 100644 index 000000000..b03344a17 --- /dev/null +++ b/tests/OpenCode.Sdk.Sandbox/OpenCode.Sdk.Sandbox.csproj @@ -0,0 +1,13 @@ + + + + Exe + $(DefaultTargetFramework) + false + + + + + + + diff --git a/tests/OpenCode.Sdk.Sandbox/Program.cs b/tests/OpenCode.Sdk.Sandbox/Program.cs new file mode 100644 index 000000000..2e0c1711f --- /dev/null +++ b/tests/OpenCode.Sdk.Sandbox/Program.cs @@ -0,0 +1,68 @@ +using System.Globalization; +using OpenCode.Sdk; +using OpenCode.Sdk.Models; + +var endpoint = Environment.GetEnvironmentVariable("OPENCODE_SANDBOX_ENDPOINT"); +if (string.IsNullOrWhiteSpace(endpoint)) +{ + await Console.Error.WriteLineAsync( + "Set OPENCODE_SANDBOX_ENDPOINT to an absolute server endpoint; the launchSettings.json profile prefills it.").ConfigureAwait(false); + await Console.Error.WriteLineAsync( + "Optional: OPENCODE_SERVER_PASSWORD (read by the SDK itself), OPENCODE_SANDBOX_SESSION_ID, OPENCODE_SANDBOX_MESSAGE_ID.").ConfigureAwait(false); + return 1; +} + +// No explicit password: the SDK resolves OPENCODE_SERVER_PASSWORD on its own at construction. +using var client = new OpenCodeClient(new Uri(endpoint)); + +var health = await client.GetHealthAsync().ConfigureAwait(false); +Console.WriteLine(string.Create( + CultureInfo.InvariantCulture, + $"health: status={health.Status} healthy={health.Health.Healthy} version={health.Health.Version} pid={health.Health.Pid}")); + +var sessionId = Environment.GetEnvironmentVariable("OPENCODE_SANDBOX_SESSION_ID"); +var messageId = Environment.GetEnvironmentVariable("OPENCODE_SANDBOX_MESSAGE_ID"); +if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(messageId)) +{ + await Console.Out.WriteLineAsync("message: skipped (set OPENCODE_SANDBOX_SESSION_ID and OPENCODE_SANDBOX_MESSAGE_ID to fetch one)").ConfigureAwait(false); + return 0; +} + +var message = await client.Sessions.GetSessionClient(sessionId).GetMessageAsync(messageId).ConfigureAwait(false); +Console.WriteLine(string.Create(CultureInfo.InvariantCulture, $"message: status={message.Status} type={message.Message.GetType().Name}")); +if (message.Message is SessionMessageAssistant assistant) +{ + Console.WriteLine($" id: {assistant.Id}"); + Console.WriteLine($" agent: {assistant.Agent}"); + Console.WriteLine($" model: {assistant.Model}"); + Console.WriteLine(string.Create(CultureInfo.InvariantCulture, $" finish: {assistant.Finish} cost: {assistant.Cost} tokens: {assistant.Tokens}")); + Console.WriteLine(string.Create(CultureInfo.InvariantCulture, $" content: {assistant.Content.Count} part(s)")); + foreach (var part in assistant.Content) + { + var summary = part switch + { + SessionMessageAssistantText text => Excerpt(text.Text), + SessionMessageAssistantReasoning reasoning => Excerpt(reasoning.Text), + UnknownSessionMessageAssistantContent unknown => $"unknown tag '{unknown.Type}'", + _ => Excerpt(part.ToString()), + }; + Console.WriteLine($" [{part.Type}] {summary}"); + } +} +else +{ + Console.WriteLine($" payload: {Excerpt(message.Message.ToString())}"); +} + +return 0; + +static string Excerpt(string? value) +{ + if (value is null) + { + return ""; + } + + var flattened = value.ReplaceLineEndings(" "); + return flattened.Length <= 160 ? flattened : $"{flattened[..160]}…"; +} diff --git a/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json b/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json new file mode 100644 index 000000000..80e9c6f2d --- /dev/null +++ b/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "sandbox": { + "commandName": "Project", + "environmentVariables": { + "OPENCODE_SANDBOX_ENDPOINT": "http://127.0.0.1:4096", + "OPENCODE_SERVER_PASSWORD": "123456", + "OPENCODE_SANDBOX_SESSION_ID": "ses_00e0d1436ffe86YL3QKbrn3Ldy", + "OPENCODE_SANDBOX_MESSAGE_ID": "msg_ffb0e86fa001n28S9q0u7F7JxM" + } + } + } +} diff --git a/tests/OpenCode.Sdk.Sandbox/README.md b/tests/OpenCode.Sdk.Sandbox/README.md new file mode 100644 index 000000000..283baac7e --- /dev/null +++ b/tests/OpenCode.Sdk.Sandbox/README.md @@ -0,0 +1,45 @@ +# OpenCode.Sdk.Sandbox + +A committed local playground for driving the SDK against a real `opencode2 serve` under a +debugger. It rides the repository's full convention set (analyzers, `.editorconfig`, format +gate) — unlike `.scratchpad/`, which remains the home for throwaway prototypes that answer a +question and disappear. + +Configuration comes from environment variables, prefilled for the IDE by +`Properties/launchSettings.json` (profile `sandbox`): + +| Variable | Meaning | +|---|---| +| `OPENCODE_SANDBOX_ENDPOINT` | Absolute server endpoint (required) | +| `OPENCODE_SERVER_PASSWORD` | Consumed by the SDK's own auth fallback, not by sandbox code | +| `OPENCODE_SANDBOX_SESSION_ID` | Optional; with a message id, fetches one message | +| `OPENCODE_SANDBOX_MESSAGE_ID` | Optional; see above | + +## Running + +Start a server with a fixed password so the checked-in profile matches (`serve` reads the +same `OPENCODE_SERVER_PASSWORD` variable the SDK falls back to; without it the server +generates and prints a random one): + +```sh +OPENCODE_SERVER_PASSWORD=123456 opencode2 serve --hostname 127.0.0.1 --port 4096 +``` + +Then F5 with the `sandbox` profile. The prefilled session/message ids belong to this +machine's local opencode history — replace them when they age out. + +## Finding ids with curl + +Session and message ids come from the pinned surface's list operations +(`v2.session.list`, `v2.message.list` — both still pending SDK breadth): + +```sh +# newest session id +curl -su opencode:123456 "http://127.0.0.1:4096/api/session?limit=1" | jq -r '.data[0].id' + +# first message id of that session +curl -su opencode:123456 "http://127.0.0.1:4096/api/session//message" | jq -r '.data[0].id' +``` + +The project deliberately stays a flat console for now; it grows a Generic Host composition +root when the Extensions package (DI) lands. diff --git a/tests/OpenCode.Sdk.Tests/EndpointPolicyTests.cs b/tests/OpenCode.Sdk.Tests/EndpointPolicyTests.cs new file mode 100644 index 000000000..6eca3ddf1 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/EndpointPolicyTests.cs @@ -0,0 +1,58 @@ +using OpenCode.Sdk.Internal; + +namespace OpenCode.Sdk.Tests; + +public sealed class EndpointPolicyTests +{ + [Test] + [Arguments("http://localhost:4096", "http://localhost:4096")] + [Arguments("http://localhost:4096/", "http://localhost:4096")] + [Arguments("https://host.example/prefix/", "https://host.example/prefix")] + [Arguments("http://host.example/a/b/", "http://host.example/a/b")] + public async Task Normalize_Should_Trim_The_Trailing_Slash_And_Preserve_The_Prefix(string endpoint, string expected) + { + var normalized = EndpointPolicy.Normalize(new Uri(endpoint)); + + await Assert.That(normalized).IsEqualTo(expected); + } + + [Test] + public async Task Normalize_Should_Refuse_A_Relative_Endpoint() + { + var exception = Assert.Throws(() => _ = EndpointPolicy.Normalize(new Uri("/api", UriKind.Relative))); + + await Assert.That(exception.Message).Contains("absolute"); + } + + [Test] + public async Task Normalize_Should_Refuse_A_Non_Http_Scheme() + { + var exception = Assert.Throws(() => _ = EndpointPolicy.Normalize(new Uri("ftp://host"))); + + await Assert.That(exception.Message).Contains("scheme"); + } + + [Test] + public async Task Normalize_Should_Refuse_A_Query() + { + var exception = Assert.Throws(() => _ = EndpointPolicy.Normalize(new Uri("http://host?x=1"))); + + await Assert.That(exception.Message).Contains("query"); + } + + [Test] + public async Task Normalize_Should_Refuse_A_Fragment() + { + var exception = Assert.Throws(() => _ = EndpointPolicy.Normalize(new Uri("http://host#frag"))); + + await Assert.That(exception.Message).Contains("fragment"); + } + + [Test] + public async Task Normalize_Should_Refuse_User_Information() + { + var exception = Assert.Throws(() => _ = EndpointPolicy.Normalize(new Uri("http://user:pw@host"))); + + await Assert.That(exception.Message).Contains("user information"); + } +} diff --git a/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/empty-assistant-marker.json b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/empty-assistant-marker.json new file mode 100644 index 000000000..1ead1f0e6 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/empty-assistant-marker.json @@ -0,0 +1 @@ +{"type":"","text":"orphaned"} diff --git a/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-assistant-content.json b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-assistant-content.json index b9f4ec4a2..e01795fda 100644 --- a/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-assistant-content.json +++ b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-assistant-content.json @@ -1 +1 @@ -{"id":"part-1","text":"answer","type":"text"} +{"text":"answer","type":"text"} diff --git a/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-compaction-message.json b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-compaction-message.json new file mode 100644 index 000000000..99906fac6 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-compaction-message.json @@ -0,0 +1 @@ +{"id":"message-4","time":{"created":1},"status":"running","reason":"auto","summary":"condensed","recent":"tail","type":"compaction"} diff --git a/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-tool-state.json b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-tool-state.json index c3fd5c77a..b61c9af06 100644 --- a/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-tool-state.json +++ b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-tool-state.json @@ -1 +1 @@ -{"input":"queued input","status":"pending"} +{"input":{"query":"queued input"},"metadata":{"elapsed":1},"status":"running"} diff --git a/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/unknown-compaction-status.json b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/unknown-compaction-status.json new file mode 100644 index 000000000..ca6dd5129 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/unknown-compaction-status.json @@ -0,0 +1 @@ +{"details":{"phase":"later"},"status":"paused","type":"compaction"} diff --git a/tests/OpenCode.Sdk.Tests/ModuleInitializer.cs b/tests/OpenCode.Sdk.Tests/ModuleInitializer.cs new file mode 100644 index 000000000..22f21765f --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/ModuleInitializer.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; + +namespace OpenCode.Sdk.Tests; + +internal static class ModuleInitializer +{ + [ModuleInitializer] + public static void Initialize() + { + UseProjectRelativeDirectory("Snapshots"); + } +} diff --git a/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj b/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj index 935638c33..2743a1cc8 100644 --- a/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj +++ b/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj @@ -13,6 +13,8 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,6 +25,17 @@ + + + + + + + + + + + diff --git a/tests/OpenCode.Sdk.Tests/OpenCodeApiExceptionTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeApiExceptionTests.cs new file mode 100644 index 000000000..267882b7f --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/OpenCodeApiExceptionTests.cs @@ -0,0 +1,49 @@ +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk.Tests; + +public sealed class OpenCodeApiExceptionTests +{ + [Test] + public async Task Constructor_Should_Carry_Status_Error_And_RawBody() + { + var error = new UnauthorizedError + { + Message = "password required", + }; + + var exception = new OpenCodeApiException("The opencode API returned status 401.", 401, error, "{\"_tag\":\"UnauthorizedError\"}"); + + await Assert.That(exception.Message).IsEqualTo("The opencode API returned status 401."); + await Assert.That(exception.Status).IsEqualTo(401); + await Assert.That(exception.Error).IsSameReferenceAs(error); + await Assert.That(exception.RawBody).IsEqualTo("{\"_tag\":\"UnauthorizedError\"}"); + } + + [Test] + public async Task Constructor_Should_Leave_Api_Data_Empty_On_The_Standard_Overloads() + { + var exception = new OpenCodeApiException("boom"); + + await Assert.That(exception.Status).IsEqualTo(0); + await Assert.That(exception.Error).IsNull(); + await Assert.That(exception.RawBody).IsNull(); + } + + [Test] + public async Task Exception_Should_Derive_From_The_OpenCode_Base() + { + await Assert.That(new OpenCodeApiException()).IsAssignableTo(); + await Assert.That(new OpenCodeTransportException()).IsAssignableTo(); + } + + [Test] + public async Task Constructor_Should_Preserve_The_Inner_Exception() + { + var inner = new InvalidOperationException("root cause"); + + var exception = new OpenCodeApiException("boom", inner); + + await Assert.That(exception.InnerException).IsSameReferenceAs(inner); + } +} diff --git a/tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs new file mode 100644 index 000000000..83b1d9c19 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs @@ -0,0 +1,330 @@ +using System.Net; +using OpenCode.Sdk.Models; +using OpenCode.Sdk.Tests.Support; + +namespace OpenCode.Sdk.Tests; + +public sealed class OpenCodeClientContractTests +{ + private static readonly Uri Endpoint = new("http://localhost:4096"); + + [Test] + public async Task GetHealthAsync_Should_Return_The_Typed_Payload() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.OK, + "{\"healthy\":true,\"version\":\"0.0.0-test\",\"pid\":42}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var response = await client.GetHealthAsync(); + + await Assert.That(response.Status).IsEqualTo(200); + await Assert.That(response.IsError).IsFalse(); + await Assert.That(response.RawBody).IsNull(); + await Assert.That(response.Health.Healthy).IsTrue(); + await Assert.That(response.Health.Version).IsEqualTo("0.0.0-test"); + await Assert.That(response.Health.Pid).IsEqualTo(42); + await Assert.That(handler.Requests.Single().RequestUri).IsEqualTo(new Uri("http://localhost:4096/api/health")); + await Assert.That(handler.Requests.Single().Method).IsEqualTo(HttpMethod.Get); + } + + [Test] + public async Task GetHealthAsync_Should_Throw_The_Typed_Error_By_Default() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.Unauthorized, + "{\"_tag\":\"UnauthorizedError\",\"message\":\"password required\"}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var exception = await Assert + .That(async () => _ = await client.GetHealthAsync()) + .Throws(); + + await Assert.That(exception!.Status).IsEqualTo(401); + await Assert.That(exception.Error).IsTypeOf(); + await Assert.That(((UnauthorizedError)exception.Error!).Message).IsEqualTo("password required"); + await Assert.That(exception.RawBody).Contains("UnauthorizedError"); + } + + [Test] + public async Task GetMessageAsync_Should_Unwrap_The_Data_Envelope() + { + var payload = new FixtureLoader().LoadJson("Serialization.known-session-message.json"); + using var handler = new RecordingHttpHandler(_ => JsonResponse(HttpStatusCode.OK, $"{{\"data\":{payload}}}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var response = await client.Sessions.GetSessionClient("ses_1 x").GetMessageAsync("msg_2/y"); + + await Assert.That(response.Status).IsEqualTo(200); + await Assert.That(response.Message).IsTypeOf(); + await Assert.That(((SessionMessageUser)response.Message).Id).IsEqualTo("message-1"); + await Assert.That(handler.Requests.Single().RequestUri!.AbsoluteUri) + .IsEqualTo("http://localhost:4096/api/session/ses_1%20x/message/msg_2%2Fy"); + } + + [Test] + public async Task GetMessageAsync_Should_Type_Both_Declared_404_Errors() + { + using var sessionHandler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.NotFound, + "{\"_tag\":\"SessionNotFoundError\",\"sessionID\":\"s\",\"message\":\"gone\"}")); + using var sessionHttpClient = new HttpClient(sessionHandler); + using var sessionClient = CreateClient(sessionHttpClient); + using var messageHandler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.NotFound, + "{\"_tag\":\"MessageNotFoundError\",\"sessionID\":\"s\",\"messageID\":\"m\",\"message\":\"gone\"}")); + using var messageHttpClient = new HttpClient(messageHandler); + using var messageClient = CreateClient(messageHttpClient); + + var sessionMiss = await Assert + .That(async () => _ = await sessionClient.Sessions.GetSessionClient("s").GetMessageAsync("m")) + .Throws(); + var messageMiss = await Assert + .That(async () => _ = await messageClient.Sessions.GetSessionClient("s").GetMessageAsync("m")) + .Throws(); + + await Assert.That(sessionMiss!.Error).IsTypeOf(); + await Assert.That(messageMiss!.Error).IsTypeOf(); + } + + [Test] + public async Task GetMessageAsync_Should_Return_The_Error_Envelope_When_NoThrow() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.NotFound, + "{\"_tag\":\"SessionNotFoundError\",\"sessionID\":\"s\",\"message\":\"gone\"}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var response = await client.Sessions.GetSessionClient("s").GetMessageAsync("m", OpenCodeRequestOptions.NoThrow); + + await Assert.That(response.IsError).IsTrue(); + await Assert.That(response.Status).IsEqualTo(404); + await Assert.That(response.Error).IsTypeOf(); + await Assert.That(response.RawBody).Contains("SessionNotFoundError"); + } + + [Test] + public async Task GetMessageAsync_Should_Downgrade_An_Unknown_Tag_To_The_Carrier() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.NotFound, + "{\"_tag\":\"BrandNewError\",\"detail\":7}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var exception = await Assert + .That(async () => _ = await client.Sessions.GetSessionClient("s").GetMessageAsync("m")) + .Throws(); + + var unknown = (UnknownOpenCodeError)exception!.Error!; + await Assert.That(unknown.Tag).IsEqualTo("BrandNewError"); + await Assert.That(unknown.Payload.GetProperty("detail").GetInt32()).IsEqualTo(7); + } + + [Test] + public async Task GetMessageAsync_Should_Downgrade_An_Undeclared_Status_To_The_Carrier() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + (HttpStatusCode)418, + "{\"_tag\":\"UnauthorizedError\",\"message\":\"weird\"}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var exception = await Assert + .That(async () => _ = await client.Sessions.GetSessionClient("s").GetMessageAsync("m")) + .Throws(); + + await Assert.That(exception!.Status).IsEqualTo(418); + await Assert.That(exception.Error).IsTypeOf(); + await Assert.That(((UnknownOpenCodeError)exception.Error!).Tag).IsEqualTo("UnauthorizedError"); + } + + [Test] + public async Task GetMessageAsync_Should_Downgrade_A_Known_Tag_At_The_Wrong_Status() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.BadRequest, + "{\"_tag\":\"SessionNotFoundError\",\"sessionID\":\"s\",\"message\":\"misplaced\"}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var exception = await Assert + .That(async () => _ = await client.Sessions.GetSessionClient("s").GetMessageAsync("m")) + .Throws(); + + await Assert.That(exception!.Status).IsEqualTo(400); + await Assert.That(exception.Error).IsTypeOf(); + await Assert.That(((UnknownOpenCodeError)exception.Error!).Tag).IsEqualTo("SessionNotFoundError"); + } + + [Test] + public async Task GetHealthAsync_Should_Preserve_The_Raw_Body_For_Malformed_Errors() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.BadRequest, + "not json")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var thrown = await Assert + .That(async () => _ = await client.GetHealthAsync()) + .Throws(); + await Assert.That(thrown!.Error).IsNull(); + await Assert.That(thrown.RawBody).IsEqualTo("not json"); + + using var noThrowHandler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.BadRequest, + "not json")); + using var noThrowHttpClient = new HttpClient(noThrowHandler); + using var noThrowClient = CreateClient(noThrowHttpClient); + var response = await noThrowClient.GetHealthAsync(OpenCodeRequestOptions.NoThrow); + await Assert.That(response.Error).IsNull(); + await Assert.That(response.RawBody).IsEqualTo("not json"); + } + + [Test] + public async Task GetHealthAsync_Should_Treat_A_Malformed_Success_As_A_Protocol_Failure() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse(HttpStatusCode.OK, "not json")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + _ = await Assert + .That(async () => _ = await client.GetHealthAsync()) + .Throws(); + } + + [Test] + public async Task GetHealthAsync_Should_Treat_An_Undeclared_2xx_As_A_Protocol_Failure() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.Created, + "{\"healthy\":true,\"version\":\"0.0.0-test\",\"pid\":42}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + _ = await Assert + .That(async () => _ = await client.GetHealthAsync()) + .Throws(); + + _ = await Assert + .That(async () => _ = await client.GetHealthAsync(OpenCodeRequestOptions.NoThrow)) + .Throws(); + } + + [Test] + public async Task GetHealthAsync_Should_Preserve_The_Raw_Body_For_An_Empty_Error_Tag() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.BadRequest, + "{\"_tag\":\"\"}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var response = await client.GetHealthAsync(OpenCodeRequestOptions.NoThrow); + + await Assert.That(response.IsError).IsTrue(); + await Assert.That(response.Error).IsNull(); + await Assert.That(response.RawBody).IsEqualTo("{\"_tag\":\"\"}"); + } + + [Test] + public async Task GetMessageAsync_Should_Treat_An_Empty_Success_Marker_As_A_Protocol_Failure() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.OK, + "{\"data\":{\"type\":\"\",\"id\":\"m\"}}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + _ = await Assert + .That(async () => _ = await client.Sessions.GetSessionClient("s").GetMessageAsync("m")) + .Throws(); + } + + [Test] + public async Task GetMessageAsync_Should_Escape_Route_Values_Into_The_Request_Uri() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.OK, + "{\"data\":{\"id\":\"message-1\",\"time\":{\"created\":1},\"text\":\"hello\",\"type\":\"user\"}}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + _ = await client.Sessions.GetSessionClient("a b").GetMessageAsync("c/d"); + + await Assert.That(handler.Requests.Single().RequestUri!.AbsoluteUri) + .IsEqualTo("http://localhost:4096/api/session/a%20b/message/c%2Fd"); + } + + [Test] + public async Task Payload_Should_Guard_Access_And_Printing_On_The_Error_Path() + { + using var handler = new RecordingHttpHandler(static _ => JsonResponse( + HttpStatusCode.Unauthorized, + "{\"_tag\":\"UnauthorizedError\",\"message\":\"no\"}")); + using var httpClient = new HttpClient(handler); + using var client = CreateClient(httpClient); + + var response = await client.GetHealthAsync(OpenCodeRequestOptions.NoThrow); + + _ = Assert.Throws(() => _ = response.Health); + var printed = response.ToString(); + await Assert.That(printed).Contains("401"); + await Assert.That(printed).DoesNotContain("Health = "); + } + + [Test] + public async Task Client_Should_Fail_Instructively_On_The_Unoverridden_Mock_Seam() + { + using var client = new MockableClient(); + + var exception = await Assert + .That(async () => _ = await client.GetHealthAsync()) + .Throws(); + + await Assert.That(exception!.Message).Contains("mocking constructor"); + _ = Assert.Throws(() => _ = client.Sessions); + } + + [Test] + public async Task Dispose_Should_Make_The_Bound_Handles_Unusable() + { + var client = new OpenCodeClient(Endpoint); + var session = client.Sessions.GetSessionClient("s"); + client.Dispose(); + + _ = await Assert + .That(async () => _ = await session.GetMessageAsync("m")) + .Throws(); + } + + [Test] + public async Task Constructor_Should_Require_An_Endpoint_For_A_Byo_Client() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + + _ = Assert.Throws(() => _ = new OpenCodeClient(httpClient, new OpenCodeClientOptions())); + } + + private static OpenCodeClient CreateClient(HttpClient httpClient) => + new(httpClient, new OpenCodeClientOptions + { + Endpoint = Endpoint, + }); + + private sealed class MockableClient : OpenCodeClient + { + } + + private static HttpResponseMessage JsonResponse(HttpStatusCode status, string body) => + new(status) + { + Content = new StringContent(body), + }; +} diff --git a/tests/OpenCode.Sdk.Tests/OpenCodeRequestOptionsTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeRequestOptionsTests.cs new file mode 100644 index 000000000..c1b3f461d --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/OpenCodeRequestOptionsTests.cs @@ -0,0 +1,24 @@ +namespace OpenCode.Sdk.Tests; + +public sealed class OpenCodeRequestOptionsTests +{ + [Test] + public async Task Constructor_Should_Default_To_Throwing_Behavior() + { + var options = new OpenCodeRequestOptions(); + + await Assert.That(options.ErrorBehavior).IsEqualTo(ErrorBehavior.Default); + } + + [Test] + public async Task NoThrow_Should_Select_The_NoThrow_Behavior() + { + await Assert.That(OpenCodeRequestOptions.NoThrow.ErrorBehavior).IsEqualTo(ErrorBehavior.NoThrow); + } + + [Test] + public async Task NoThrow_Should_Return_A_Shared_Instance() + { + await Assert.That(OpenCodeRequestOptions.NoThrow).IsSameReferenceAs(OpenCodeRequestOptions.NoThrow); + } +} diff --git a/tests/OpenCode.Sdk.Tests/OpenCodeResponseTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeResponseTests.cs new file mode 100644 index 000000000..f6625148c --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/OpenCodeResponseTests.cs @@ -0,0 +1,20 @@ +namespace OpenCode.Sdk.Tests; + +public sealed class OpenCodeResponseTests +{ + [Test] + public async Task Constructor_Should_Default_To_The_Success_Path() + { + var response = new EmptyResponse + { + Status = 200, + }; + + await Assert.That(response.Status).IsEqualTo(200); + await Assert.That(response.IsError).IsFalse(); + await Assert.That(response.Error).IsNull(); + await Assert.That(response.RawBody).IsNull(); + } + + private sealed record EmptyResponse : OpenCodeResponse; +} diff --git a/tests/OpenCode.Sdk.Tests/OpenCodeRoutesTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeRoutesTests.cs new file mode 100644 index 000000000..718bf5f22 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/OpenCodeRoutesTests.cs @@ -0,0 +1,34 @@ +namespace OpenCode.Sdk.Tests; + +public sealed class OpenCodeRoutesTests +{ + [Test] + [Arguments(".")] + [Arguments("..")] + [Arguments(" ")] + public async Task GetMessage_Should_Refuse_An_Unsafe_Session_Segment(string sessionId) + { + var exception = Assert.Throws(() => _ = OpenCodeRoutes.Sessions.GetMessage(sessionId, "m")); + + await Assert.That(exception.ParamName).IsEqualTo("sessionId"); + } + + [Test] + [Arguments(".")] + [Arguments("..")] + [Arguments(" ")] + public async Task GetMessage_Should_Refuse_An_Unsafe_Message_Segment(string messageId) + { + var exception = Assert.Throws(() => _ = OpenCodeRoutes.Sessions.GetMessage("s", messageId)); + + await Assert.That(exception.ParamName).IsEqualTo("messageId"); + } + + [Test] + public async Task GetMessage_Should_Escape_Reserved_Characters() + { + var route = OpenCodeRoutes.Sessions.GetMessage("a b", "c/d"); + + await Assert.That(route).IsEqualTo("/api/session/a%20b/message/c%2Fd"); + } +} diff --git a/tests/OpenCode.Sdk.Tests/PipelineTests.cs b/tests/OpenCode.Sdk.Tests/PipelineTests.cs new file mode 100644 index 000000000..3e20177e3 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/PipelineTests.cs @@ -0,0 +1,380 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using NSubstitute; +using OpenCode.Sdk.Internal; +using OpenCode.Sdk.Internal.Abstractions; +using OpenCode.Sdk.Models; +using OpenCode.Sdk.Tests.Support; + +namespace OpenCode.Sdk.Tests; + +public sealed class PipelineTests +{ + private const string PasswordVariable = "OPENCODE_SERVER_PASSWORD"; + private static readonly Uri Endpoint = new("http://localhost:4096"); + + [Test] + public async Task ExecuteAsync_Should_Join_The_Route_Onto_The_Endpoint() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + + _ = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None); + + await Assert.That(handler.Requests.Single().Method).IsEqualTo(HttpMethod.Get); + await Assert.That(handler.Requests.Single().RequestUri).IsEqualTo(new Uri("http://localhost:4096/api/health")); + } + + [Test] + public async Task ExecuteAsync_Should_Preserve_The_Endpoint_Path_Prefix() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient, endpoint: new Uri("http://localhost:8080/opencode/")); + + _ = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None); + + await Assert.That(handler.Requests.Single().RequestUri).IsEqualTo(new Uri("http://localhost:8080/opencode/api/health")); + } + + [Test] + public async Task ExecuteAsync_Should_Decorate_Basic_Auth_From_The_Explicit_Password() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + var environment = Substitute.For(); + using var pipeline = CreatePipeline(httpClient, password: "secret", environment: environment); + + _ = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None); + + var expected = $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes("opencode:secret"))}"; + await Assert.That(handler.Requests.Single().Authorization).IsEqualTo(expected); + _ = environment.DidNotReceive().GetEnvironmentVariable(Arg.Any()); + } + + [Test] + public async Task ExecuteAsync_Should_Resolve_The_Environment_Password_Once_At_Construction() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + var environment = Substitute.For(); + _ = environment.GetEnvironmentVariable(PasswordVariable).Returns("fallback"); + using var pipeline = CreatePipeline(httpClient, environment: environment); + + _ = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None); + _ = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None); + + var expected = $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes("opencode:fallback"))}"; + await Assert.That(handler.Requests.All(request => request.Authorization == expected)).IsTrue(); + _ = environment.Received(1).GetEnvironmentVariable(PasswordVariable); + } + + [Test] + public async Task ExecuteAsync_Should_Send_Anonymously_When_No_Password_Resolves() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + + _ = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None); + + await Assert.That(handler.Requests.Single().Authorization).IsNull(); + } + + [Test] + public async Task ExecuteAsync_Should_Decorate_The_User_Agent_Per_Request() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + + _ = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None); + + await Assert.That(handler.Requests.Single().UserAgent).IsEqualTo(UserAgentPolicy.Resolve().ToString()); + } + + [Test] + public async Task ExecuteAsync_Should_Never_Touch_The_Client_Defaults() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient, password: "secret"); + + _ = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None); + + await Assert.That(httpClient.BaseAddress).IsNull(); + await Assert.That(httpClient.DefaultRequestHeaders.Any()).IsFalse(); + } + + [Test] + public async Task ExecuteAsync_Should_Buffer_The_Body_For_The_Adapter() + { + using var handler = new RecordingHttpHandler(static _ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"healthy\":true}"), + }); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + var adapter = new RecordingResponseAdapter(); + + var response = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", adapter, options: null, CancellationToken.None); + + await Assert.That(adapter.AdaptedStatus).IsEqualTo(200); + await Assert.That(adapter.AdaptedRawBody).IsEqualTo("{\"healthy\":true}"); + await Assert.That(response.Status).IsEqualTo(200); + } + + [Test] + public async Task ExecuteAsync_Should_Throw_The_Api_Exception_By_Default() + { + using var handler = new RecordingHttpHandler(static _ => new HttpResponseMessage(HttpStatusCode.Unauthorized) + { + Content = new StringContent("{\"_tag\":\"UnauthorizedError\"}"), + }); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + var error = new UnauthorizedError + { + Message = "password required", + }; + var adapter = new RecordingResponseAdapter((status, rawBody) => new TestResponse + { + Status = status, + IsError = true, + Error = error, + RawBody = rawBody, + }); + + var exception = await Assert + .That(async () => _ = await pipeline.ExecuteAsync(HttpMethod.Get, "/api/health", adapter, options: null, CancellationToken.None)) + .Throws(); + + await Assert.That(exception!.Status).IsEqualTo(401); + await Assert.That(exception.Error).IsSameReferenceAs(error); + await Assert.That(exception.RawBody).IsEqualTo("{\"_tag\":\"UnauthorizedError\"}"); + await Assert.That(exception.Message).Contains("401"); + await Assert.That(exception.Message).Contains("UnauthorizedError"); + } + + [Test] + public async Task ExecuteAsync_Should_Return_The_Error_Envelope_When_NoThrow() + { + using var handler = new RecordingHttpHandler(static _ => new HttpResponseMessage(HttpStatusCode.Unauthorized) + { + Content = new StringContent("{\"_tag\":\"UnauthorizedError\"}"), + }); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + var adapter = new RecordingResponseAdapter(static (status, rawBody) => new TestResponse + { + Status = status, + IsError = true, + RawBody = rawBody, + }); + + var response = await pipeline.ExecuteAsync( + HttpMethod.Get, + "/api/health", + adapter, + OpenCodeRequestOptions.NoThrow, + CancellationToken.None); + + await Assert.That(response.IsError).IsTrue(); + await Assert.That(response.Status).IsEqualTo(401); + await Assert.That(response.RawBody).IsEqualTo("{\"_tag\":\"UnauthorizedError\"}"); + } + + [Test] + public async Task ExecuteAsync_Should_Wrap_Network_Failures_As_Transport_Failures() + { + using var handler = new RecordingHttpHandler(static _ => throw new HttpRequestException("connection refused")); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + + var exception = await Assert + .That(async () => _ = await pipeline.ExecuteAsync( + HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None)) + .Throws(); + + await Assert.That(exception!.InnerException).IsTypeOf(); + } + + [Test] + public async Task ExecuteAsync_Should_Wrap_An_Invalid_Charset_As_A_Transport_Failure() + { + using var handler = new RecordingHttpHandler(static _ => + { + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent("{\"healthy\":true}"u8.ToArray()), + }; + response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "not-an-encoding" }; + return response; + }); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + + var exception = await Assert + .That(async () => _ = await pipeline.ExecuteAsync( + HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None)) + .Throws(); + + await Assert.That(exception!.InnerException).IsTypeOf(); + } + + [Test] + public async Task ExecuteAsync_Should_Wrap_A_Disposal_Race_On_Send_As_A_Transport_Failure() + { + using var handler = new RecordingHttpHandler(static _ => throw new ObjectDisposedException(nameof(HttpClient))); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + + var exception = await Assert + .That(async () => _ = await pipeline.ExecuteAsync( + HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None)) + .Throws(); + + await Assert.That(exception!.InnerException).IsTypeOf(); + } + + [Test] + public async Task ExecuteAsync_Should_Refuse_An_Undefined_Error_Behavior_Before_Sending() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + var options = new OpenCodeRequestOptions { ErrorBehavior = (ErrorBehavior)2 }; + + _ = await Assert + .That(async () => _ = await pipeline.ExecuteAsync( + HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options, CancellationToken.None)) + .Throws(); + + await Assert.That(handler.Requests.Any()).IsFalse(); + } + + [Test] + public async Task ExecuteAsync_Should_Pass_Cancellation_Through() + { + using var handler = new RecordingHttpHandler(static _ => throw new OperationCanceledException()); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + + _ = await Assert + .That(async () => _ = await pipeline.ExecuteAsync( + HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None)) + .Throws(); + } + + [Test] + public async Task Dispose_Should_Dispose_The_Owned_Client() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + var pipeline = CreatePipeline(httpClient, ownsHttpClient: true); + + pipeline.Dispose(); + + await Assert.That(handler.IsDisposed).IsTrue(); + } + + [Test] + public async Task Dispose_Should_Never_Dispose_An_Injected_Client() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + var pipeline = CreatePipeline(httpClient); + + pipeline.Dispose(); + + await Assert.That(handler.IsDisposed).IsFalse(); + } + + [Test] + public async Task ExecuteAsync_Should_Refuse_After_Dispose() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + var pipeline = CreatePipeline(httpClient); + pipeline.Dispose(); + + _ = await Assert + .That(async () => _ = await pipeline.ExecuteAsync( + HttpMethod.Get, "/api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None)) + .Throws(); + } + + [Test] + public async Task ExecuteAsync_Should_Refuse_A_Route_Without_A_Leading_Slash() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + using var pipeline = CreatePipeline(httpClient); + + _ = await Assert + .That(async () => _ = await pipeline.ExecuteAsync( + HttpMethod.Get, "api/health", new RecordingResponseAdapter(), options: null, CancellationToken.None)) + .Throws(); + } + + [Test] + public async Task Create_Should_Refuse_An_Injected_Client_Without_An_Endpoint() + { + using var handler = new RecordingHttpHandler(); + using var httpClient = new HttpClient(handler); + + var exception = Assert.Throws(() => _ = Pipeline.Create( + httpClient, + new OpenCodeClientOptions(), + Substitute.For())); + + await Assert.That(exception.Message).Contains("Endpoint"); + } + + [Test] + public async Task Create_Should_Refuse_A_Conflicting_Options_Endpoint() + { + var options = new OpenCodeClientOptions + { + Endpoint = new Uri("http://other:1"), + }; + + var exception = Assert.Throws(() => _ = Pipeline.Create( + Endpoint, + options, + Substitute.For())); + + await Assert.That(exception.Message).Contains("endpoint"); + } + + [Test] + public async Task Create_Should_Build_An_Owned_Pipeline_From_The_Endpoint() + { + using var pipeline = Pipeline.Create(Endpoint, options: null, Substitute.For()); + + await Assert.That(pipeline).IsNotNull(); + } + + [Test] + public async Task UserAgent_Should_Match_The_Assembly_Informational_Version() + { + var informational = typeof(Pipeline).Assembly + .GetCustomAttribute()?.InformationalVersion; + + var product = UserAgentPolicy.Resolve().ToString(); + + await Assert.That(informational).IsNotNull(); + await Assert.That(product).IsEqualTo($"OpenCode.Sdk/{informational!.Split('+')[0]}"); + } + + private static Pipeline CreatePipeline( + HttpClient httpClient, + bool ownsHttpClient = false, + Uri? endpoint = null, + string? password = null, + IEnvironmentProvider? environment = null) => + new(httpClient, ownsHttpClient, endpoint ?? Endpoint, password, environment ?? Substitute.For()); +} diff --git a/tests/OpenCode.Sdk.Tests/PublicApiBaselineTests.cs b/tests/OpenCode.Sdk.Tests/PublicApiBaselineTests.cs new file mode 100644 index 000000000..73c04ebcf --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/PublicApiBaselineTests.cs @@ -0,0 +1,21 @@ +using PublicApiGenerator; + +namespace OpenCode.Sdk.Tests; + +public sealed class PublicApiBaselineTests +{ + [Test] + public async Task PublicApi_Should_Match_The_Reviewed_Baseline() + { + var api = typeof(OpenCodeClient).Assembly.GeneratePublicApi(new ApiGeneratorOptions + { + IncludeAssemblyAttributes = false, + }); + + // Every target framework compiles the same public surface; one reviewed baseline + // covers them all. + var settings = new VerifySettings(); + settings.UseFileName("PublicApi"); + await Verify(api, settings); + } +} diff --git a/tests/OpenCode.Sdk.Tests/ResponseAdapterTests.cs b/tests/OpenCode.Sdk.Tests/ResponseAdapterTests.cs new file mode 100644 index 000000000..749315bb4 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/ResponseAdapterTests.cs @@ -0,0 +1,114 @@ +using System.Text.Json.Serialization.Metadata; +using OpenCode.Sdk.Internal; +using OpenCode.Sdk.Internal.Serialization; +using OpenCode.Sdk.Models; +using OpenCode.Sdk.Tests.Support; + +namespace OpenCode.Sdk.Tests; + +public sealed class ResponseAdapterTests +{ + private static readonly string[] UnauthorizedTags = ["UnauthorizedError"]; + + [Test] + public async Task ReadBarePayload_Should_Deserialize_The_Body() + { + var payload = ProbeAdapter.Bare("{\"healthy\":true,\"version\":\"1\",\"pid\":7}", OpenCodeJsonContext.Default.ServiceHealth); + + await Assert.That(payload.Healthy).IsTrue(); + await Assert.That(payload.Pid).IsEqualTo(7); + } + + [Test] + public async Task ReadBarePayload_Should_Treat_Malformed_Bodies_As_Protocol_Failures() + { + _ = Assert.Throws(() => + _ = ProbeAdapter.Bare("not json", OpenCodeJsonContext.Default.ServiceHealth)); + } + + [Test] + public async Task ReadBarePayload_Should_Treat_Null_Bodies_As_Protocol_Failures() + { + _ = Assert.Throws(() => + _ = ProbeAdapter.Bare("null", OpenCodeJsonContext.Default.ServiceHealth)); + } + + [Test] + public async Task ReadDataPayload_Should_Unwrap_The_Data_Envelope() + { + var payload = ProbeAdapter.Data( + "{\"data\":{\"healthy\":false,\"version\":\"2\",\"pid\":9}}", + OpenCodeJsonContext.Default.ServiceHealth); + + await Assert.That(payload.Healthy).IsFalse(); + await Assert.That(payload.Version).IsEqualTo("2"); + } + + [Test] + public async Task ReadDataPayload_Should_Refuse_A_Missing_Data_Property() + { + _ = Assert.Throws(() => + _ = ProbeAdapter.Data("{\"other\":1}", OpenCodeJsonContext.Default.ServiceHealth)); + } + + [Test] + public async Task ReadTolerantError_Should_Keep_A_Known_Tag_At_Its_Declared_Status() + { + var error = ProbeAdapter.Error("{\"_tag\":\"UnauthorizedError\",\"message\":\"no\"}", UnauthorizedTags); + + await Assert.That(error).IsTypeOf(); + } + + [Test] + public async Task ReadTolerantError_Should_Keep_The_Unknown_Carrier_For_An_Unknown_Tag() + { + var error = ProbeAdapter.Error("{\"_tag\":\"BrandNewError\",\"detail\":1}", UnauthorizedTags); + + await Assert.That(error).IsTypeOf(); + await Assert.That(((UnknownOpenCodeError)error!).Tag).IsEqualTo("BrandNewError"); + } + + [Test] + public async Task ReadTolerantError_Should_Downgrade_A_Known_Tag_At_The_Wrong_Status() + { + var error = ProbeAdapter.Error("{\"_tag\":\"SessionNotFoundError\",\"sessionID\":\"s\",\"message\":\"m\"}", UnauthorizedTags); + + await Assert.That(error).IsTypeOf(); + await Assert.That(((UnknownOpenCodeError)error!).Tag).IsEqualTo("SessionNotFoundError"); + await Assert.That(((UnknownOpenCodeError)error).Payload.GetProperty("sessionID").GetString()).IsEqualTo("s"); + } + + [Test] + public async Task ReadTolerantError_Should_Downgrade_A_Known_Tag_At_An_Undeclared_Status() + { + var error = ProbeAdapter.Error("{\"_tag\":\"UnauthorizedError\",\"message\":\"no\"}", allowedTags: null); + + await Assert.That(error).IsTypeOf(); + } + + [Test] + public async Task ReadTolerantError_Should_Return_Null_For_A_Malformed_Body() + { + var error = ProbeAdapter.Error("oops", UnauthorizedTags); + + await Assert.That(error).IsNull(); + } + + private sealed class ProbeAdapter : ResponseAdapter + { + public static TPayload Bare(string rawBody, JsonTypeInfo typeInfo) => + ReadBarePayload(rawBody, typeInfo); + + public static TPayload Data(string rawBody, JsonTypeInfo typeInfo) => + ReadDataPayload(rawBody, typeInfo); + + public static OpenCodeError? Error(string rawBody, IReadOnlyCollection? allowedTags) => + ReadTolerantError(rawBody, allowedTags); + + public override TestResponse Adapt(int status, string rawBody) => + new() + { + Status = status, + }; + } +} diff --git a/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs b/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs index ab7fbe3b4..b16352a8c 100644 --- a/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs +++ b/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs @@ -14,28 +14,76 @@ public async Task Deserialize_Should_Create_Known_Outer_Variant_When_Marker_Is_L { var json = _fixtures.LoadJson("Serialization.known-session-message.json"); - var result = _serializer.Deserialize(json); + var result = _serializer.Deserialize(json); await Assert.That(result).IsTypeOf(); var user = (SessionMessageUser)result; - await Assert.That(user.ID).IsEqualTo("message-1"); + await Assert.That(user.Id).IsEqualTo("message-1"); await Assert.That(user.Text).IsEqualTo("hello"); - var serialized = _serializer.Serialize(user); + var serialized = _serializer.Serialize(user); using var document = JsonDocument.Parse(serialized); await Assert.That(document.RootElement.GetProperty("type").GetString()).IsEqualTo("user"); } + [Test] + public async Task Deserialize_Should_Normalize_Absent_Optional_Nonnull_Collections_To_Empty() + { + var json = _fixtures.LoadJson("Serialization.known-session-message.json"); + + var result = _serializer.Deserialize(json); + + await Assert.That(result).IsTypeOf(); + var user = (SessionMessageUser)result; + await Assert.That(user.Metadata).IsEmpty(); + await Assert.That(user.Files).IsEmpty(); + await Assert.That(user.Agents).IsEmpty(); + await Assert.That(user.Skills).IsEmpty(); + } + [Test] public async Task Deserialize_Should_Preserve_Unknown_Outer_Variant_Byte_For_Byte() { var json = _fixtures.LoadJson("Serialization.unknown-session-message.json"); - var result = _serializer.Deserialize(json); + var result = _serializer.Deserialize(json); - await Assert.That(result).IsTypeOf(); - var unknown = (UnknownSessionMessage)result; + await Assert.That(result).IsTypeOf(); + var unknown = (UnknownSessionMessageInfo)result; await Assert.That(unknown.Type).IsEqualTo("future-message"); - var roundTrip = _serializer.Serialize(unknown); + var roundTrip = _serializer.Serialize(unknown); + await Assert.That(roundTrip).IsEqualTo(json); + } + + [Test] + public async Task Deserialize_Should_Create_Nested_Compaction_Variant_Through_The_Outer_Union() + { + var json = _fixtures.LoadJson("Serialization.known-compaction-message.json"); + + var result = _serializer.Deserialize(json); + + await Assert.That(result).IsTypeOf(); + var running = (SessionMessageCompactionRunning)result; + await Assert.That(running.Type).IsEqualTo("compaction"); + await Assert.That(running.Status).IsEqualTo("running"); + await Assert.That(running.Reason).IsEqualTo(SessionMessageCompactionRunningReason.Auto); + var serialized = _serializer.Serialize(running); + using var document = JsonDocument.Parse(serialized); + await Assert.That(document.RootElement.GetProperty("type").GetString()).IsEqualTo("compaction"); + await Assert.That(document.RootElement.GetProperty("status").GetString()).IsEqualTo("running"); + } + + [Test] + public async Task Deserialize_Should_Preserve_Unknown_Compaction_Status_Byte_For_Byte() + { + var json = _fixtures.LoadJson("Serialization.unknown-compaction-status.json"); + + var result = _serializer.Deserialize(json); + + await Assert.That(result).IsTypeOf(); + var unknown = (UnknownSessionMessageCompaction)result; + await Assert.That(unknown.Type).IsEqualTo("compaction"); + await Assert.That(unknown.Status).IsEqualTo("paused"); + var roundTrip = _serializer.Serialize(unknown); await Assert.That(roundTrip).IsEqualTo(json); } @@ -76,12 +124,12 @@ public async Task Deserialize_Should_Create_Known_Tool_State_When_Marker_Is_Last var result = _serializer.Deserialize(json); - await Assert.That(result).IsTypeOf(); - var pending = (SessionMessageToolStatePending)result; - await Assert.That(pending.Input).IsEqualTo("queued input"); - var serialized = _serializer.Serialize(pending); + await Assert.That(result).IsTypeOf(); + var running = (SessionMessageToolStateRunning)result; + await Assert.That(running.Input["query"].GetString()).IsEqualTo("queued input"); + var serialized = _serializer.Serialize(running); using var document = JsonDocument.Parse(serialized); - await Assert.That(document.RootElement.GetProperty("status").GetString()).IsEqualTo("pending"); + await Assert.That(document.RootElement.GetProperty("status").GetString()).IsEqualTo("running"); } [Test] @@ -107,6 +155,15 @@ public async Task Deserialize_Should_Throw_When_Union_Marker_Is_Missing() .Throws(); } + [Test] + public async Task Deserialize_Should_Throw_When_Union_Marker_Is_Empty() + { + var json = _fixtures.LoadJson("Serialization.empty-assistant-marker.json"); + + _ = await Assert.That(() => _serializer.Deserialize(json)) + .Throws(); + } + [Test] public async Task Deserialize_Should_Throw_When_Union_Marker_Has_The_Wrong_Type() { @@ -121,7 +178,7 @@ public async Task Deserialize_Should_Reject_Explicit_Null_For_Optional_Nonnull_C { var json = _fixtures.LoadJson("Serialization.null-optional-collection.json"); - _ = await Assert.That(() => _serializer.Deserialize(json)) + _ = await Assert.That(() => _serializer.Deserialize(json)) .Throws(); } @@ -135,35 +192,30 @@ public async Task Deserialize_Should_Throw_JsonException_When_Union_Payload_Is_N } [Test] - public async Task ProviderMetadata_Should_Recursively_Copy_Collections() + public async Task State_Should_Defensively_Copy_The_Input_Collection() { - var inner = new Dictionary(StringComparer.Ordinal) + var state = new Dictionary(StringComparer.Ordinal) { ["original"] = default, }; - var outer = new Dictionary>(StringComparer.Ordinal) - { - ["provider"] = inner, - }; var model = new SessionMessageAssistantReasoning { - ID = "part-1", Text = "reasoning", - ProviderMetadata = outer, + State = state, }; - inner.Add("inner-mutation", default); - outer.Add("outer-mutation", new Dictionary(StringComparer.Ordinal)); + state.Add("mutation", default); - await Assert.That(model.ProviderMetadata.ContainsKey("outer-mutation")).IsFalse(); - await Assert.That(model.ProviderMetadata["provider"].ContainsKey("inner-mutation")).IsFalse(); + await Assert.That(model.State.ContainsKey("mutation")).IsFalse(); + await Assert.That(model.State.ContainsKey("original")).IsTrue(); } [Test] public async Task GetTypeInfo_Should_Resolve_Union_Metadata_Without_Reflection_Fallback() { await Assert.That(JsonSerializer.IsReflectionEnabledByDefault).IsFalse(); - await Assert.That(_serializer.GetTypeInfo(typeof(SessionMessage))).IsNotNull(); + await Assert.That(_serializer.GetTypeInfo(typeof(SessionMessageInfo))).IsNotNull(); + await Assert.That(_serializer.GetTypeInfo(typeof(SessionMessageCompaction))).IsNotNull(); await Assert.That(_serializer.GetTypeInfo(typeof(SessionMessageAssistantContent))).IsNotNull(); await Assert.That(_serializer.GetTypeInfo(typeof(SessionMessageToolState))).IsNotNull(); } diff --git a/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt b/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt new file mode 100644 index 000000000..2690f098c --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt @@ -0,0 +1,928 @@ +namespace OpenCode.Sdk +{ + public enum ErrorBehavior + { + Default = 0, + NoThrow = 1, + } + public sealed class HealthResponse : OpenCode.Sdk.OpenCodeResponse, System.IEquatable + { + public HealthResponse() { } + public required OpenCode.Sdk.Models.ServiceHealth Health { get; init; } + protected override bool PrintMembers(System.Text.StringBuilder builder) { } + } + public class OpenCodeApiException : OpenCode.Sdk.OpenCodeException + { + public OpenCodeApiException() { } + public OpenCodeApiException(string message) { } + public OpenCodeApiException(string message, System.Exception innerException) { } + public OpenCodeApiException(string message, int status, OpenCode.Sdk.Models.OpenCodeError? error, string? rawBody) { } + public OpenCode.Sdk.Models.OpenCodeError? Error { get; } + public string? RawBody { get; } + public int Status { get; } + } + public class OpenCodeClient : System.IDisposable + { + protected OpenCodeClient() { } + public OpenCodeClient(System.Uri endpoint) { } + public OpenCodeClient(System.Net.Http.HttpClient httpClient, OpenCode.Sdk.OpenCodeClientOptions options) { } + public OpenCodeClient(System.Uri endpoint, OpenCode.Sdk.OpenCodeClientOptions? options) { } + public virtual OpenCode.Sdk.SessionsClient Sessions { get; } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public virtual System.Threading.Tasks.Task GetHealthAsync(OpenCode.Sdk.OpenCodeRequestOptions? options = null, System.Threading.CancellationToken cancellationToken = default) { } + } + public sealed class OpenCodeClientOptions + { + public OpenCodeClientOptions() { } + public System.Uri? Endpoint { get; set; } + public string? Password { get; set; } + } + public class OpenCodeException : System.Exception + { + public OpenCodeException() { } + public OpenCodeException(string message) { } + public OpenCodeException(string message, System.Exception innerException) { } + } + public sealed class OpenCodeRequestOptions + { + public OpenCodeRequestOptions() { } + public OpenCode.Sdk.ErrorBehavior ErrorBehavior { get; init; } + public static OpenCode.Sdk.OpenCodeRequestOptions NoThrow { get; } + } + public abstract class OpenCodeResponse : System.IEquatable + { + protected OpenCodeResponse() { } + public OpenCode.Sdk.Models.OpenCodeError? Error { get; init; } + public bool IsError { get; init; } + public string? RawBody { get; init; } + public required int Status { get; init; } + } + public static class OpenCodeRoutes + { + public static class Health + { + public const string Get = "/api/health"; + } + public static class Sessions + { + public const string GetMessageTemplate = "/api/session/{sessionID}/message/{messageID}"; + public static string GetMessage(string sessionId, string messageId) { } + } + } + public class OpenCodeTransportException : OpenCode.Sdk.OpenCodeException + { + public OpenCodeTransportException() { } + public OpenCodeTransportException(string message) { } + public OpenCodeTransportException(string message, System.Exception innerException) { } + } + public class SessionClient + { + protected SessionClient() { } + public virtual System.Threading.Tasks.Task GetMessageAsync(string messageId, OpenCode.Sdk.OpenCodeRequestOptions? options = null, System.Threading.CancellationToken cancellationToken = default) { } + } + public sealed class SessionMessageResponse : OpenCode.Sdk.OpenCodeResponse, System.IEquatable + { + public SessionMessageResponse() { } + public required OpenCode.Sdk.Models.SessionMessageInfo Message { get; init; } + protected override bool PrintMembers(System.Text.StringBuilder builder) { } + } + public class SessionsClient + { + protected SessionsClient() { } + public virtual OpenCode.Sdk.SessionClient GetSessionClient(string sessionId) { } + } +} +namespace OpenCode.Sdk.Models +{ + public sealed class InvalidRequestError : OpenCode.Sdk.Models.OpenCodeError, System.IEquatable + { + public InvalidRequestError() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("field")] + public string? Field { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("kind")] + public string? Kind { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("message")] + public required string Message { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("_tag")] + public override string Tag { get; } + } + public sealed class LocationRef : System.IEquatable + { + public LocationRef() { } + [System.Text.Json.Serialization.JsonPropertyName("directory")] + public required string Directory { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("workspaceID")] + public string? WorkspaceId { get; init; } + } + public sealed class MessageNotFoundError : OpenCode.Sdk.Models.OpenCodeError, System.IEquatable + { + public MessageNotFoundError() { } + [System.Text.Json.Serialization.JsonPropertyName("message")] + public required string Message { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("messageID")] + public required string MessageId { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("sessionID")] + public required string SessionId { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("_tag")] + public override string Tag { get; } + } + public sealed class ModelRef : System.IEquatable + { + public ModelRef() { } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("providerID")] + public required string ProviderId { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("variant")] + public string? Variant { get; init; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.OpenCodeErrorJsonConverter))] + public abstract class OpenCodeError : System.IEquatable + { + protected OpenCodeError() { } + [System.Text.Json.Serialization.JsonPropertyName("_tag")] + public abstract string Tag { get; } + } + public sealed class PromptAgentAttachment : System.IEquatable + { + public PromptAgentAttachment() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("mention")] + public OpenCode.Sdk.Models.PromptMention? Mention { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("name")] + public required string Name { get; init; } + } + public sealed class PromptFileAttachment : System.IEquatable + { + public PromptFileAttachment() { } + [System.Text.Json.Serialization.JsonPropertyName("data")] + public required string Data { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string? Description { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("mention")] + public OpenCode.Sdk.Models.PromptMention? Mention { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("mime")] + public required string Mime { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? Name { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("source")] + public required OpenCode.Sdk.Models.PromptFileSource Source { get; init; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.PromptFileSourceJsonConverter))] + public abstract class PromptFileSource : System.IEquatable + { + protected PromptFileSource() { } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public abstract string Type { get; } + } + public sealed class PromptFileSourceInline : OpenCode.Sdk.Models.PromptFileSource, System.IEquatable + { + public PromptFileSourceInline() { } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class PromptFileSourceUri : OpenCode.Sdk.Models.PromptFileSource, System.IEquatable + { + public PromptFileSourceUri() { } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + [System.Text.Json.Serialization.JsonPropertyName("uri")] + public required System.Uri Uri { get; init; } + } + public sealed class PromptMention : System.IEquatable + { + public PromptMention() { } + [System.Text.Json.Serialization.JsonPropertyName("end")] + public required double End { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("start")] + public required double Start { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("text")] + public required string Text { get; init; } + } + public sealed class PromptSkillAttachment : System.IEquatable + { + public PromptSkillAttachment() { } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("mention")] + public OpenCode.Sdk.Models.PromptMention? Mention { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("name")] + public required string Name { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("text")] + public required string Text { get; init; } + } + public sealed class ServiceHealth : System.IEquatable + { + public ServiceHealth() { } + [System.Text.Json.Serialization.JsonPropertyName("healthy")] + public required bool Healthy { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("pid")] + public required long Pid { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("version")] + public required string Version { get; init; } + } + public sealed class SessionMessageAgentSelected : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + public SessionMessageAgentSelected() { } + [System.Text.Json.Serialization.JsonPropertyName("agent")] + public required string Agent { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("previous")] + public string? Previous { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageAgentSelectedTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageAgentSelectedTime : System.IEquatable + { + public SessionMessageAgentSelectedTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageAssistant : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + public SessionMessageAssistant() { } + [System.Text.Json.Serialization.JsonPropertyName("agent")] + public required string Agent { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("content")] + public required System.Collections.Generic.IReadOnlyList Content { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("cost")] + public double? Cost { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("error")] + public OpenCode.Sdk.Models.SessionStructuredError? Error { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("finish")] + public OpenCode.Sdk.Models.SessionMessageAssistantFinish? Finish { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("model")] + public required OpenCode.Sdk.Models.ModelRef Model { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("retry")] + public OpenCode.Sdk.Models.SessionMessageAssistantRetry? Retry { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("snapshot")] + public OpenCode.Sdk.Models.SessionMessageAssistantSnapshot? Snapshot { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageAssistantTime Time { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("tokens")] + public OpenCode.Sdk.Models.TokenUsageInfo? Tokens { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.SessionMessageAssistantContentJsonConverter))] + public abstract class SessionMessageAssistantContent : System.IEquatable + { + protected SessionMessageAssistantContent() { } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public abstract string Type { get; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public enum SessionMessageAssistantFinish + { + [System.Text.Json.Serialization.JsonStringEnumMemberName("stop")] + Stop = 0, + [System.Text.Json.Serialization.JsonStringEnumMemberName("length")] + Length = 1, + [System.Text.Json.Serialization.JsonStringEnumMemberName("tool-calls")] + ToolCalls = 2, + [System.Text.Json.Serialization.JsonStringEnumMemberName("content-filter")] + ContentFilter = 3, + [System.Text.Json.Serialization.JsonStringEnumMemberName("error")] + Error = 4, + [System.Text.Json.Serialization.JsonStringEnumMemberName("unknown")] + Unknown = 5, + } + public sealed class SessionMessageAssistantReasoning : OpenCode.Sdk.Models.SessionMessageAssistantContent, System.IEquatable + { + public SessionMessageAssistantReasoning() { } + [System.Text.Json.Serialization.JsonPropertyName("state")] + public System.Collections.Generic.IReadOnlyDictionary State { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("text")] + public required string Text { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("time")] + public OpenCode.Sdk.Models.SessionMessageAssistantReasoningTime? Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageAssistantReasoningTime : System.IEquatable + { + public SessionMessageAssistantReasoningTime() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("completed")] + public double? Completed { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageAssistantRetry : System.IEquatable + { + public SessionMessageAssistantRetry() { } + [System.Text.Json.Serialization.JsonPropertyName("at")] + public required double At { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("attempt")] + public required long Attempt { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("error")] + public required OpenCode.Sdk.Models.SessionStructuredError Error { get; init; } + } + public sealed class SessionMessageAssistantSnapshot : System.IEquatable + { + public SessionMessageAssistantSnapshot() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("end")] + public string? End { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("files")] + public System.Collections.Generic.IReadOnlyList Files { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("start")] + public string? Start { get; init; } + } + public sealed class SessionMessageAssistantText : OpenCode.Sdk.Models.SessionMessageAssistantContent, System.IEquatable + { + public SessionMessageAssistantText() { } + [System.Text.Json.Serialization.JsonPropertyName("state")] + public System.Collections.Generic.IReadOnlyDictionary State { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("text")] + public required string Text { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageAssistantTime : System.IEquatable + { + public SessionMessageAssistantTime() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("completed")] + public double? Completed { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageAssistantTool : OpenCode.Sdk.Models.SessionMessageAssistantContent, System.IEquatable + { + public SessionMessageAssistantTool() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("executed")] + public bool? Executed { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("name")] + public required string Name { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("providerResultState")] + public System.Collections.Generic.IReadOnlyDictionary ProviderResultState { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("providerState")] + public System.Collections.Generic.IReadOnlyDictionary ProviderState { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("state")] + public required OpenCode.Sdk.Models.SessionMessageToolState State { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageAssistantToolTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageAssistantToolTime : System.IEquatable + { + public SessionMessageAssistantToolTime() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("completed")] + public double? Completed { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("ran")] + public double? Ran { get; init; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.SessionMessageCompactionJsonConverter))] + public abstract class SessionMessageCompaction : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + protected SessionMessageCompaction() { } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public abstract string Status { get; } + } + public sealed class SessionMessageCompactionCompleted : OpenCode.Sdk.Models.SessionMessageCompaction, System.IEquatable + { + public SessionMessageCompactionCompleted() { } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("reason")] + public required OpenCode.Sdk.Models.SessionMessageCompactionCompletedReason Reason { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("recent")] + public required string Recent { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public override string Status { get; } + [System.Text.Json.Serialization.JsonPropertyName("summary")] + public required string Summary { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageCompactionCompletedTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public enum SessionMessageCompactionCompletedReason + { + [System.Text.Json.Serialization.JsonStringEnumMemberName("auto")] + Auto = 0, + [System.Text.Json.Serialization.JsonStringEnumMemberName("manual")] + Manual = 1, + } + public sealed class SessionMessageCompactionCompletedTime : System.IEquatable + { + public SessionMessageCompactionCompletedTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageCompactionFailed : OpenCode.Sdk.Models.SessionMessageCompaction, System.IEquatable + { + public SessionMessageCompactionFailed() { } + [System.Text.Json.Serialization.JsonPropertyName("error")] + public required OpenCode.Sdk.Models.SessionStructuredError Error { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("reason")] + public required OpenCode.Sdk.Models.SessionMessageCompactionFailedReason Reason { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public override string Status { get; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageCompactionFailedTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public enum SessionMessageCompactionFailedReason + { + [System.Text.Json.Serialization.JsonStringEnumMemberName("auto")] + Auto = 0, + [System.Text.Json.Serialization.JsonStringEnumMemberName("manual")] + Manual = 1, + } + public sealed class SessionMessageCompactionFailedTime : System.IEquatable + { + public SessionMessageCompactionFailedTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageCompactionRunning : OpenCode.Sdk.Models.SessionMessageCompaction, System.IEquatable + { + public SessionMessageCompactionRunning() { } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("reason")] + public required OpenCode.Sdk.Models.SessionMessageCompactionRunningReason Reason { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("recent")] + public required string Recent { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public override string Status { get; } + [System.Text.Json.Serialization.JsonPropertyName("summary")] + public required string Summary { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageCompactionRunningTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public enum SessionMessageCompactionRunningReason + { + [System.Text.Json.Serialization.JsonStringEnumMemberName("auto")] + Auto = 0, + [System.Text.Json.Serialization.JsonStringEnumMemberName("manual")] + Manual = 1, + } + public sealed class SessionMessageCompactionRunningTime : System.IEquatable + { + public SessionMessageCompactionRunningTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.SessionMessageInfoJsonConverter))] + public abstract class SessionMessageInfo : System.IEquatable + { + protected SessionMessageInfo() { } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public abstract string Type { get; } + } + public sealed class SessionMessageLocationSwitched : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + public SessionMessageLocationSwitched() { } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("location")] + public required OpenCode.Sdk.Models.LocationRef Location { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("previous")] + public OpenCode.Sdk.Models.SessionMessageLocationSwitchedPrevious? Previous { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("projectID")] + public string? ProjectId { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("subpath")] + public string? Subpath { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageLocationSwitchedTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageLocationSwitchedPrevious : System.IEquatable + { + public SessionMessageLocationSwitchedPrevious() { } + [System.Text.Json.Serialization.JsonPropertyName("location")] + public required OpenCode.Sdk.Models.LocationRef Location { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("projectID")] + public string? ProjectId { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("subpath")] + public string? Subpath { get; init; } + } + public sealed class SessionMessageLocationSwitchedTime : System.IEquatable + { + public SessionMessageLocationSwitchedTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageModelSelected : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + public SessionMessageModelSelected() { } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("model")] + public required OpenCode.Sdk.Models.ModelRef Model { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("previous")] + public OpenCode.Sdk.Models.ModelRef? Previous { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageModelSelectedTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageModelSelectedTime : System.IEquatable + { + public SessionMessageModelSelectedTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageShell : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + public SessionMessageShell() { } + [System.Text.Json.Serialization.JsonPropertyName("command")] + public required string Command { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("exit")] + public double? Exit { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("output")] + public OpenCode.Sdk.Models.SessionMessageShellOutput? Output { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("shellID")] + public required string ShellId { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public required OpenCode.Sdk.Models.SessionMessageShellStatus Status { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageShellTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageShellOutput : System.IEquatable + { + public SessionMessageShellOutput() { } + [System.Text.Json.Serialization.JsonPropertyName("cursor")] + public required long Cursor { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("output")] + public required string Output { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("size")] + public required long Size { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("truncated")] + public required bool Truncated { get; init; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public enum SessionMessageShellStatus + { + [System.Text.Json.Serialization.JsonStringEnumMemberName("running")] + Running = 0, + [System.Text.Json.Serialization.JsonStringEnumMemberName("exited")] + Exited = 1, + [System.Text.Json.Serialization.JsonStringEnumMemberName("timeout")] + Timeout = 2, + [System.Text.Json.Serialization.JsonStringEnumMemberName("killed")] + Killed = 3, + } + public sealed class SessionMessageShellTime : System.IEquatable + { + public SessionMessageShellTime() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("completed")] + public double? Completed { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageSkill : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + public SessionMessageSkill() { } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("name")] + public required string Name { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("skill")] + public required string Skill { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("text")] + public required string Text { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageSkillTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageSkillTime : System.IEquatable + { + public SessionMessageSkillTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageSynthetic : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + public SessionMessageSynthetic() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string? Description { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("text")] + public required string Text { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageSyntheticTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageSyntheticTime : System.IEquatable + { + public SessionMessageSyntheticTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionMessageSystem : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + public SessionMessageSystem() { } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string? Description { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("text")] + public required string Text { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageSystemTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageSystemTime : System.IEquatable + { + public SessionMessageSystemTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.SessionMessageToolStateJsonConverter))] + public abstract class SessionMessageToolState : System.IEquatable + { + protected SessionMessageToolState() { } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public abstract string Status { get; } + } + public sealed class SessionMessageToolStateCompleted : OpenCode.Sdk.Models.SessionMessageToolState, System.IEquatable + { + public SessionMessageToolStateCompleted() { } + [System.Text.Json.Serialization.JsonPropertyName("content")] + public required System.Collections.Generic.IReadOnlyList Content { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("input")] + public required System.Collections.Generic.IReadOnlyDictionary Input { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public override string Status { get; } + } + public sealed class SessionMessageToolStateError : OpenCode.Sdk.Models.SessionMessageToolState, System.IEquatable + { + public SessionMessageToolStateError() { } + [System.Text.Json.Serialization.JsonPropertyName("content")] + public System.Collections.Generic.IReadOnlyList Content { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("error")] + public required OpenCode.Sdk.Models.SessionStructuredError Error { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("input")] + public required System.Collections.Generic.IReadOnlyDictionary Input { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public override string Status { get; } + } + public sealed class SessionMessageToolStateRunning : OpenCode.Sdk.Models.SessionMessageToolState, System.IEquatable + { + public SessionMessageToolStateRunning() { } + [System.Text.Json.Serialization.JsonPropertyName("input")] + public required System.Collections.Generic.IReadOnlyDictionary Input { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public required System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public override string Status { get; } + } + public sealed class SessionMessageToolStateStreaming : OpenCode.Sdk.Models.SessionMessageToolState, System.IEquatable + { + public SessionMessageToolStateStreaming() { } + [System.Text.Json.Serialization.JsonPropertyName("input")] + public required string Input { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public override string Status { get; } + } + public sealed class SessionMessageUser : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + public SessionMessageUser() { } + [System.Text.Json.Serialization.JsonPropertyName("agents")] + public System.Collections.Generic.IReadOnlyList Agents { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("files")] + public System.Collections.Generic.IReadOnlyList Files { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("id")] + public required string Id { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("metadata")] + public System.Collections.Generic.IReadOnlyDictionary Metadata { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("skills")] + public System.Collections.Generic.IReadOnlyList Skills { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("text")] + public required string Text { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("time")] + public required OpenCode.Sdk.Models.SessionMessageUserTime Time { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class SessionMessageUserTime : System.IEquatable + { + public SessionMessageUserTime() { } + [System.Text.Json.Serialization.JsonPropertyName("created")] + public required double Created { get; init; } + } + public sealed class SessionNotFoundError : OpenCode.Sdk.Models.OpenCodeError, System.IEquatable + { + public SessionNotFoundError() { } + [System.Text.Json.Serialization.JsonPropertyName("message")] + public required string Message { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("sessionID")] + public required string SessionId { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("_tag")] + public override string Tag { get; } + } + public sealed class SessionStructuredError : System.IEquatable + { + public SessionStructuredError() { } + [System.Text.Json.Serialization.JsonPropertyName("message")] + public required string Message { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("status")] + public long? Status { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public required string Type { get; init; } + } + public sealed class TokenUsageInfo : System.IEquatable + { + public TokenUsageInfo() { } + [System.Text.Json.Serialization.JsonPropertyName("cache")] + public required OpenCode.Sdk.Models.TokenUsageInfoCache Cache { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("input")] + public required double Input { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("output")] + public required double Output { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("reasoning")] + public required double Reasoning { get; init; } + } + public sealed class TokenUsageInfoCache : System.IEquatable + { + public TokenUsageInfoCache() { } + [System.Text.Json.Serialization.JsonPropertyName("read")] + public required double Read { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("write")] + public required double Write { get; init; } + } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.ToolContentJsonConverter))] + public abstract class ToolContent : System.IEquatable + { + protected ToolContent() { } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public abstract string Type { get; } + } + public sealed class ToolFileContent : OpenCode.Sdk.Models.ToolContent, System.IEquatable + { + public ToolFileContent() { } + [System.Text.Json.Serialization.JsonPropertyName("mime")] + public required string Mime { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? Name { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + [System.Text.Json.Serialization.JsonPropertyName("uri")] + public required System.Uri Uri { get; init; } + } + public sealed class ToolTextContent : OpenCode.Sdk.Models.ToolContent, System.IEquatable + { + public ToolTextContent() { } + [System.Text.Json.Serialization.JsonPropertyName("text")] + public required string Text { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class UnauthorizedError : OpenCode.Sdk.Models.OpenCodeError, System.IEquatable + { + public UnauthorizedError() { } + [System.Text.Json.Serialization.JsonPropertyName("message")] + public required string Message { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("_tag")] + public override string Tag { get; } + } + public sealed class UnknownOpenCodeError : OpenCode.Sdk.Models.OpenCodeError, System.IEquatable + { + [System.Text.Json.Serialization.JsonConstructor] + public UnknownOpenCodeError(string tag, System.Text.Json.JsonElement payload) { } + public System.Text.Json.JsonElement Payload { get; } + [System.Text.Json.Serialization.JsonPropertyName("_tag")] + public override string Tag { get; } + } + public sealed class UnknownPromptFileSource : OpenCode.Sdk.Models.PromptFileSource, System.IEquatable + { + [System.Text.Json.Serialization.JsonConstructor] + public UnknownPromptFileSource(string type, System.Text.Json.JsonElement payload) { } + public System.Text.Json.JsonElement Payload { get; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class UnknownSessionMessageAssistantContent : OpenCode.Sdk.Models.SessionMessageAssistantContent, System.IEquatable + { + [System.Text.Json.Serialization.JsonConstructor] + public UnknownSessionMessageAssistantContent(string type, System.Text.Json.JsonElement payload) { } + public System.Text.Json.JsonElement Payload { get; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class UnknownSessionMessageCompaction : OpenCode.Sdk.Models.SessionMessageCompaction, System.IEquatable + { + [System.Text.Json.Serialization.JsonConstructor] + public UnknownSessionMessageCompaction(string status, System.Text.Json.JsonElement payload) { } + public System.Text.Json.JsonElement Payload { get; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public override string Status { get; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class UnknownSessionMessageInfo : OpenCode.Sdk.Models.SessionMessageInfo, System.IEquatable + { + [System.Text.Json.Serialization.JsonConstructor] + public UnknownSessionMessageInfo(string type, System.Text.Json.JsonElement payload) { } + public System.Text.Json.JsonElement Payload { get; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } + public sealed class UnknownSessionMessageToolState : OpenCode.Sdk.Models.SessionMessageToolState, System.IEquatable + { + [System.Text.Json.Serialization.JsonConstructor] + public UnknownSessionMessageToolState(string status, System.Text.Json.JsonElement payload) { } + public System.Text.Json.JsonElement Payload { get; } + [System.Text.Json.Serialization.JsonPropertyName("status")] + public override string Status { get; } + } + public sealed class UnknownToolContent : OpenCode.Sdk.Models.ToolContent, System.IEquatable + { + [System.Text.Json.Serialization.JsonConstructor] + public UnknownToolContent(string type, System.Text.Json.JsonElement payload) { } + public System.Text.Json.JsonElement Payload { get; } + [System.Text.Json.Serialization.JsonPropertyName("type")] + public override string Type { get; } + } +} \ No newline at end of file diff --git a/tests/OpenCode.Sdk.Tests/Support/GeneratedJsonSerializer.cs b/tests/OpenCode.Sdk.Tests/Support/GeneratedJsonSerializer.cs index b79459179..c2a90ba6d 100644 --- a/tests/OpenCode.Sdk.Tests/Support/GeneratedJsonSerializer.cs +++ b/tests/OpenCode.Sdk.Tests/Support/GeneratedJsonSerializer.cs @@ -35,7 +35,7 @@ public JsonTypeInfo GetTypeInfo(Type type) private static JsonSerializerContext LoadContext() { - var contextType = typeof(SessionMessage).Assembly.GetType(ContextTypeName, throwOnError: true, ignoreCase: false) + var contextType = typeof(SessionMessageInfo).Assembly.GetType(ContextTypeName, throwOnError: true, ignoreCase: false) ?? throw new InvalidOperationException($"The generated JSON context '{ContextTypeName}' was not found."); var defaultProperty = contextType.GetProperty("Default", BindingFlags.Public | BindingFlags.Static) ?? throw new InvalidOperationException("The generated JSON context has no Default property."); diff --git a/tests/OpenCode.Sdk.Tests/Support/RecordedRequest.cs b/tests/OpenCode.Sdk.Tests/Support/RecordedRequest.cs new file mode 100644 index 000000000..4aef9b079 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Support/RecordedRequest.cs @@ -0,0 +1,12 @@ +namespace OpenCode.Sdk.Tests.Support; + +internal sealed record RecordedRequest +{ + public required Uri? RequestUri { get; init; } + + public required HttpMethod Method { get; init; } + + public string? Authorization { get; init; } + + public string? UserAgent { get; init; } +} diff --git a/tests/OpenCode.Sdk.Tests/Support/RecordingHttpHandler.cs b/tests/OpenCode.Sdk.Tests/Support/RecordingHttpHandler.cs new file mode 100644 index 000000000..1eafbe11d --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Support/RecordingHttpHandler.cs @@ -0,0 +1,46 @@ +using System.Net; + +namespace OpenCode.Sdk.Tests.Support; + +internal sealed class RecordingHttpHandler : HttpMessageHandler +{ + private readonly Func _responder; + private readonly List _requests = []; + + public RecordingHttpHandler() + : this(static _ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}"), + }) + { + } + + public RecordingHttpHandler(Func responder) + { + ArgumentNullException.ThrowIfNull(responder); + + _responder = responder; + } + + public IReadOnlyList Requests => _requests; + + public bool IsDisposed { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + _requests.Add(new RecordedRequest + { + RequestUri = request.RequestUri, + Method = request.Method, + Authorization = request.Headers.Authorization?.ToString(), + UserAgent = request.Headers.UserAgent.Count is 0 ? null : request.Headers.UserAgent.ToString(), + }); + return Task.FromResult(_responder(request)); + } + + protected override void Dispose(bool disposing) + { + IsDisposed = true; + base.Dispose(disposing); + } +} diff --git a/tests/OpenCode.Sdk.Tests/Support/RecordingResponseAdapter.cs b/tests/OpenCode.Sdk.Tests/Support/RecordingResponseAdapter.cs new file mode 100644 index 000000000..15a36e57f --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Support/RecordingResponseAdapter.cs @@ -0,0 +1,34 @@ +using OpenCode.Sdk.Internal; + +namespace OpenCode.Sdk.Tests.Support; + +internal sealed class RecordingResponseAdapter : ResponseAdapter +{ + private readonly Func _map; + + public RecordingResponseAdapter() + : this(static (status, _) => new TestResponse + { + Status = status, + }) + { + } + + public RecordingResponseAdapter(Func map) + { + ArgumentNullException.ThrowIfNull(map); + + _map = map; + } + + public int? AdaptedStatus { get; private set; } + + public string? AdaptedRawBody { get; private set; } + + public override TestResponse Adapt(int status, string rawBody) + { + AdaptedStatus = status; + AdaptedRawBody = rawBody; + return _map(status, rawBody); + } +} diff --git a/tests/OpenCode.Sdk.Tests/Support/TestResponse.cs b/tests/OpenCode.Sdk.Tests/Support/TestResponse.cs new file mode 100644 index 000000000..b94976b15 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Support/TestResponse.cs @@ -0,0 +1,3 @@ +namespace OpenCode.Sdk.Tests.Support; + +internal sealed record TestResponse : OpenCodeResponse; diff --git a/tests/OpenCode.Sdk.Tests/SystemEnvironmentProviderTests.cs b/tests/OpenCode.Sdk.Tests/SystemEnvironmentProviderTests.cs new file mode 100644 index 000000000..60e30a9c6 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/SystemEnvironmentProviderTests.cs @@ -0,0 +1,37 @@ +using OpenCode.Sdk.Internal; + +namespace OpenCode.Sdk.Tests; + +public sealed class SystemEnvironmentProviderTests +{ + [Test] + public async Task GetEnvironmentVariable_Should_Read_The_Process_Environment() + { + const string name = "OPENCODE_SDK_TEST_VARIABLE"; + Environment.SetEnvironmentVariable(name, "expected"); + try + { + var value = new SystemEnvironmentProvider().GetEnvironmentVariable(name); + + await Assert.That(value).IsEqualTo("expected"); + } + finally + { + Environment.SetEnvironmentVariable(name, null); + } + } + + [Test] + public async Task GetEnvironmentVariable_Should_Return_Null_When_The_Variable_Is_Absent() + { + var value = new SystemEnvironmentProvider().GetEnvironmentVariable("OPENCODE_SDK_TEST_ABSENT_VARIABLE"); + + await Assert.That(value).IsNull(); + } + + [Test] + public async Task GetEnvironmentVariable_Should_Refuse_A_Blank_Name() + { + _ = Assert.Throws(() => _ = new SystemEnvironmentProvider().GetEnvironmentVariable(" ")); + } +} diff --git a/tests/OpenCode.Sdk.Tests/UserAgentPolicyTests.cs b/tests/OpenCode.Sdk.Tests/UserAgentPolicyTests.cs new file mode 100644 index 000000000..065cac260 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/UserAgentPolicyTests.cs @@ -0,0 +1,39 @@ +using OpenCode.Sdk.Internal; + +namespace OpenCode.Sdk.Tests; + +public sealed class UserAgentPolicyTests +{ + [Test] + [Arguments("1.2.3", "OpenCode.Sdk/1.2.3")] + [Arguments("1.0.0-alpha.3", "OpenCode.Sdk/1.0.0-alpha.3")] + [Arguments("1.0.0+4f2a91cde800", "OpenCode.Sdk/1.0.0")] + [Arguments("1.0.0-alpha+4f2a91cde800", "OpenCode.Sdk/1.0.0-alpha")] + public async Task Compose_Should_Strip_Build_Metadata(string informationalVersion, string expected) + { + var product = UserAgentPolicy.Compose(informationalVersion); + + await Assert.That(product.ToString()).IsEqualTo(expected); + } + + [Test] + [Arguments(null)] + [Arguments("")] + [Arguments(" ")] + [Arguments("not a version ###")] + [Arguments("+onlymetadata")] + public async Task Compose_Should_Omit_The_Version_Token_When_The_Version_Is_Unusable(string? informationalVersion) + { + var product = UserAgentPolicy.Compose(informationalVersion); + + await Assert.That(product.ToString()).IsEqualTo("OpenCode.Sdk"); + } + + [Test] + public async Task Resolve_Should_Read_The_Assembly_Informational_Version() + { + var product = UserAgentPolicy.Resolve(); + + await Assert.That(product.ToString().StartsWith("OpenCode.Sdk", StringComparison.Ordinal)).IsTrue(); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs new file mode 100644 index 000000000..7c246ef0a --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs @@ -0,0 +1,20 @@ +using OpenCode.Sdk.Tools.Generator.Binding; + +namespace OpenCode.Sdk.Tools.Tests.Generator.Binding; + +public sealed class CSharpNamePolicyTests +{ + [Test] + [Arguments("id", "Id")] + [Arguments("sessionID", "SessionId")] + [Arguments("messageID", "MessageId")] + [Arguments("callID", "CallId")] + [Arguments("URL", "Url")] + [Arguments("APIError", "ApiError")] + public async Task ToPascalCase_Should_Use_Ordinary_Acronym_Casing(string wireName, string expected) + { + var result = CSharpNamePolicy.ToPascalCase(wireName); + + await Assert.That(result).IsEqualTo(expected); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs new file mode 100644 index 000000000..533fd91fe --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs @@ -0,0 +1,494 @@ +using OpenCode.Sdk.Tools.Generator.Binding.Models; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; +using OpenCode.Sdk.Tools.Tests.Support; +using static OpenCode.Sdk.Tools.Tests.Support.BindingScenarioData; + +namespace OpenCode.Sdk.Tools.Tests.Generator.Binding; + +public sealed class OperationPlanBinderTests +{ + [Test] + public async Task Bind_Should_Create_The_Selected_Pinned_Client_Plans() + { + var plan = await new BindingTestHost().BindPinnedAsync(); + + await Assert.That(plan.Clients.Select(static client => client.Name) + .SequenceEqual(["OpenCodeClient", "SessionClient", "SessionsClient"], StringComparer.Ordinal)).IsTrue(); + await Assert.That(plan.Clients.All(static client => client.Namespace == "OpenCode.Sdk")).IsTrue(); + + var root = plan.Clients.Single(static client => client.Role == ClientRole.Root); + await Assert.That(root.Name).IsEqualTo("OpenCodeClient"); + await Assert.That(root.SubClients.Single().PropertyName).IsEqualTo("Sessions"); + await Assert.That(root.SubClients.Single().TypeName).IsEqualTo("SessionsClient"); + await Assert.That(root.HandleFactory).IsNull(); + await Assert.That(root.HandleParameter).IsNull(); + + var health = root.Operations.Single(); + await Assert.That(health.MethodName).IsEqualTo("GetHealthAsync"); + await Assert.That(health.HttpMethod).IsEqualTo("get"); + await Assert.That(health.RouteTemplate).IsEqualTo("/api/health"); + await Assert.That(health.RouteContainerName).IsEqualTo("Health"); + await Assert.That(health.RouteMemberName).IsEqualTo("Get"); + await Assert.That(health.Parameters).IsEmpty(); + await Assert.That(health.Summary).IsEqualTo("Check server health"); + await Assert.That(health.Description).IsNotNull(); + await Assert.That(health.Envelope.ResponseTypeName).IsEqualTo("HealthResponse"); + await Assert.That(health.Envelope.AdapterTypeName).IsEqualTo("HealthResponseAdapter"); + await Assert.That(health.Envelope.PayloadName).IsEqualTo("Health"); + await Assert.That(health.Envelope.PayloadTypeName).IsEqualTo("ServiceHealth"); + await Assert.That(health.Envelope.HasDataEnvelope).IsFalse(); + await Assert.That(health.ErrorMap.Statuses.Select(static status => status.StatusCode) + .SequenceEqual([400, 401])).IsTrue(); + await Assert.That(health.ErrorMap.Statuses[0].Tags.Single().Tag).IsEqualTo("InvalidRequestError"); + await Assert.That(health.ErrorMap.Statuses[0].Tags.Single().TypeName).IsEqualTo("InvalidRequestError"); + await Assert.That(health.ErrorMap.Statuses[1].Tags.Single().Tag).IsEqualTo("UnauthorizedError"); + + var sessions = plan.Clients.Single(static client => client.Role == ClientRole.Collection); + await Assert.That(sessions.Name).IsEqualTo("SessionsClient"); + await Assert.That(sessions.Operations).IsEmpty(); + await Assert.That(sessions.HandleFactory!.MethodName).IsEqualTo("GetSessionClient"); + await Assert.That(sessions.HandleFactory.HandleTypeName).IsEqualTo("SessionClient"); + await Assert.That(sessions.HandleFactory.Parameter.WireName).IsEqualTo("sessionID"); + await Assert.That(sessions.HandleFactory.Parameter.Name).IsEqualTo("sessionId"); + await Assert.That(sessions.HandleFactory.Parameter.TypeName).IsEqualTo("string"); + + var session = plan.Clients.Single(static client => client.Role == ClientRole.Handle); + await Assert.That(session.Name).IsEqualTo("SessionClient"); + await Assert.That(session.HandleParameter!.WireName).IsEqualTo("sessionID"); + await Assert.That(session.HandleParameter.IsHandleParameter).IsTrue(); + + var message = session.Operations.Single(); + await Assert.That(message.MethodName).IsEqualTo("GetMessageAsync"); + await Assert.That(message.RouteTemplate).IsEqualTo("/api/session/{sessionID}/message/{messageID}"); + await Assert.That(message.RouteContainerName).IsEqualTo("Sessions"); + await Assert.That(message.RouteMemberName).IsEqualTo("GetMessage"); + await Assert.That(message.Summary).IsEqualTo("Get session message"); + await Assert.That(message.Parameters.Select(static parameter => parameter.Name) + .SequenceEqual(["sessionId", "messageId"], StringComparer.Ordinal)).IsTrue(); + await Assert.That(message.Parameters[0].IsHandleParameter).IsTrue(); + await Assert.That(message.Parameters[1].IsHandleParameter).IsFalse(); + await Assert.That(message.Parameters[1].WireName).IsEqualTo("messageID"); + await Assert.That(message.Parameters[1].TypeName).IsEqualTo("string"); + await Assert.That(message.Envelope.ResponseTypeName).IsEqualTo("SessionMessageResponse"); + await Assert.That(message.Envelope.AdapterTypeName).IsEqualTo("SessionMessageResponseAdapter"); + await Assert.That(message.Envelope.PayloadName).IsEqualTo("Message"); + await Assert.That(message.Envelope.PayloadTypeName).IsEqualTo("SessionMessageInfo"); + await Assert.That(message.Envelope.HasDataEnvelope).IsTrue(); + await Assert.That(message.ErrorMap.Statuses.Select(static status => status.StatusCode) + .SequenceEqual([400, 401, 404])).IsTrue(); + await Assert.That(message.ErrorMap.Statuses[2].Tags.Select(static tag => tag.Tag) + .SequenceEqual(["MessageNotFoundError", "SessionNotFoundError"], StringComparer.Ordinal)).IsTrue(); + await Assert.That(message.ErrorMap.Statuses[2].Tags.All(static tag => tag.Tag == tag.TypeName)).IsTrue(); + } + + [Test] + public async Task Bind_Should_Bind_A_Synthetic_Same_Shape_Group_Through_The_Same_Rules() + { + var document = await BindingTestHost.IngestAsync(GadgetScenario()); + + var plan = new BindingTestHost().Bind( + document, + Selection("v2.gadget.part"), + Curation(Groups("gadget", ClientGroup(clientName: "Gadgets", handleName: "GadgetClient", handleParameter: "gadgetID")))); + + var root = plan.Clients.Single(static client => client.Role == ClientRole.Root); + await Assert.That(root.SubClients.Single().PropertyName).IsEqualTo("Gadgets"); + await Assert.That(root.SubClients.Single().TypeName).IsEqualTo("GadgetsClient"); + + var gadgets = plan.Clients.Single(static client => client.Role == ClientRole.Collection); + await Assert.That(gadgets.HandleFactory!.MethodName).IsEqualTo("GetGadgetClient"); + await Assert.That(gadgets.HandleFactory.Parameter.Name).IsEqualTo("gadgetId"); + + var gadget = plan.Clients.Single(static client => client.Role == ClientRole.Handle); + await Assert.That(gadget.Name).IsEqualTo("GadgetClient"); + var part = gadget.Operations.Single(); + await Assert.That(part.MethodName).IsEqualTo("GetPartAsync"); + await Assert.That(part.RouteTemplate).IsEqualTo("/api/gadget/{gadgetID}/part/{partID}"); + await Assert.That(part.RouteContainerName).IsEqualTo("Gadgets"); + await Assert.That(part.RouteMemberName).IsEqualTo("GetPart"); + await Assert.That(part.Parameters.Select(static parameter => parameter.Name) + .SequenceEqual(["gadgetId", "partId"], StringComparer.Ordinal)).IsTrue(); + await Assert.That(part.Envelope.ResponseTypeName).IsEqualTo("GadgetPartResponse"); + await Assert.That(part.Envelope.AdapterTypeName).IsEqualTo("GadgetPartResponseAdapter"); + await Assert.That(part.Envelope.PayloadName).IsEqualTo("Part"); + await Assert.That(part.Envelope.PayloadTypeName).IsEqualTo("GadgetPart"); + await Assert.That(part.Envelope.HasDataEnvelope).IsTrue(); + await Assert.That(part.ErrorMap.Statuses.Single().StatusCode).IsEqualTo(404); + await Assert.That(part.ErrorMap.Statuses.Single().Tags.Single().TypeName).IsEqualTo("GadgetMissingError"); + } + + [Test] + public async Task Bind_Should_Keep_A_Group_Without_Handle_Declaration_Flat() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.widget.item", path: "/api/widget/{sessionID}/item", configure: operation => operation + .Parameter("sessionID", "path", schema => schema.Type("string"), required: true) + .Response(200, "application/json", schema => schema.Ref("ItemInfo"))))); + + var plan = new BindingTestHost().Bind( + document, + Selection("v2.widget.item"), + Curation(Groups("widget", ClientGroup(clientName: "Widgets", handleName: null, handleParameter: null)))); + + await Assert.That(plan.Clients.Any(static client => client.Role == ClientRole.Handle)).IsFalse(); + var widgets = plan.Clients.Single(static client => client.Role == ClientRole.Collection); + await Assert.That(widgets.HandleFactory).IsNull(); + var item = widgets.Operations.Single(); + await Assert.That(item.MethodName).IsEqualTo("GetItemAsync"); + await Assert.That(item.Parameters.Single().Name).IsEqualTo("sessionId"); + await Assert.That(item.Parameters.Single().IsHandleParameter).IsFalse(); + } + + [Test] + public async Task Bind_Should_Keep_Collection_Operations_On_The_Collection_Client() + { + var document = await BindingTestHost.IngestAsync(GadgetScenario(spec => spec + .WithOperation("v2.gadget.overview", path: "/api/gadget-overview", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("GadgetPart"))))); + + var plan = new BindingTestHost().Bind( + document, + Selection("v2.gadget.part", "v2.gadget.overview"), + Curation(Groups("gadget", ClientGroup(clientName: "Gadgets", handleName: "GadgetClient", handleParameter: "gadgetID")))); + + var gadgets = plan.Clients.Single(static client => client.Role == ClientRole.Collection); + await Assert.That(gadgets.Operations.Single().MethodName).IsEqualTo("GetOverviewAsync"); + var gadget = plan.Clients.Single(static client => client.Role == ClientRole.Handle); + await Assert.That(gadget.Operations.Single().MethodName).IsEqualTo("GetPartAsync"); + } + + [Test] + public async Task Bind_Should_Order_Parameters_By_Route_Template_Position() + { + var document = await BindingTestHost.IngestAsync(GadgetScenario(parametersReversed: true)); + + var plan = new BindingTestHost().Bind( + document, + Selection("v2.gadget.part"), + Curation(Groups("gadget", ClientGroup(clientName: "Gadgets", handleName: "GadgetClient", handleParameter: "gadgetID")))); + + var part = plan.Clients.Single(static client => client.Role == ClientRole.Handle).Operations.Single(); + await Assert.That(part.Parameters.Select(static parameter => parameter.WireName) + .SequenceEqual(["gadgetID", "partID"], StringComparer.Ordinal)).IsTrue(); + } + + [Test] + public async Task Bind_Should_Refuse_A_Non_Get_Operation() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.health.reset", method: "post", path: "/api/health-reset", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("ItemInfo"))))); + + await AssertOperationRefusalAsync(document, "v2.health.reset", "only GET operations"); + } + + [Test] + public async Task Bind_Should_Refuse_A_Query_Parameter() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.health.get", configure: operation => operation + .Parameter("limit", "query", schema => schema.Type("string")) + .Response(200, "application/json", schema => schema.Ref("ItemInfo"))))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "required path parameter"); + } + + [Test] + public async Task Bind_Should_Refuse_A_Request_Body() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.health.get", configure: operation => operation + .RequestBody("application/json", schema => schema.Type("object") + .Property("value", property => property.Type("string"), required: true)) + .Response(200, "application/json", schema => schema.Ref("ItemInfo"))))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "request bodies"); + } + + [Test] + public async Task Bind_Should_Refuse_Multiple_Success_Statuses() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.health.get", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("ItemInfo")) + .Response(201, "application/json", schema => schema.Ref("ItemInfo"))))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "exactly one success response"); + } + + [Test] + public async Task Bind_Should_Refuse_A_Non_200_Success() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.health.get", configure: operation => operation + .WithoutResponse(200) + .Response(201, "application/json", schema => schema.Ref("ItemInfo"))))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "status 200"); + } + + [Test] + public async Task Bind_Should_Refuse_A_Success_Without_Json_Content() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithOperation("v2.health.get"))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "JSON schema"); + } + + [Test] + public async Task Bind_Should_Refuse_An_Unsupported_Envelope_Shape() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.health.get", configure: operation => operation + .Response(200, "application/json", schema => schema.Type("object") + .Property("data", property => property.Ref("ItemInfo"), required: true) + .Property("location", property => property.Type("string"), required: true))))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "envelope shape"); + } + + [Test] + public async Task Bind_Should_Refuse_A_Bare_Success_Without_A_Named_Schema() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithOperation("v2.health.get", configure: operation => operation + .Response(200, "application/json", schema => schema.Type("object") + .Property("value", property => property.Type("string"), required: true))))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "named schema"); + } + + [Test] + public async Task Bind_Should_Refuse_An_Untagged_Error_Response() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithSchema("PlainProblem", schema => schema.Type("object") + .Property("message", property => property.Type("string"), required: true)) + .WithOperation("v2.health.get", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("ItemInfo")) + .Response(404, "application/json", schema => schema.Ref("PlainProblem"))))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "tagged error"); + } + + [Test] + public async Task Bind_Should_Refuse_An_Event_Stream_Response() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.health.get", configure: operation => operation + .SseResponse(schema => schema.Ref("ItemInfo"))))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "event-stream"); + } + + [Test] + public async Task Bind_Should_Refuse_A_Wildcard_Path() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.health.get", path: "/api/health/*", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("ItemInfo"))))); + + await AssertOperationRefusalAsync(document, "v2.health.get", "wildcard"); + } + + [Test] + public async Task Bind_Should_Refuse_Colliding_Method_Names_On_A_Client() + { + var document = await BindingTestHost.IngestAsync(GadgetScenario(spec => spec + .WithOperation("v2.gadget.get.part", path: "/api/gadget/{gadgetID}/part-alias/{partID}", configure: operation => operation + .Parameter("gadgetID", "path", schema => schema.Type("string"), required: true) + .Parameter("partID", "path", schema => schema.Type("string"), required: true) + .Response(200, "application/json", schema => schema.Type("object") + .Property("data", property => property.Ref("GadgetPart"), required: true))))); + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.gadget.part", "v2.gadget.get.part"), + Curation(Groups("gadget", ClientGroup(clientName: "Gadgets", handleName: "GadgetClient", handleParameter: "gadgetID"))))); + + await Assert.That(exception.Errors.Any(static error => error.Category == BindingErrorCategory.Naming + && error.Problem.Contains("GetPartAsync", StringComparison.Ordinal))).IsTrue(); + await Assert.That(exception.Errors.Any(static error => error.Category == BindingErrorCategory.Naming + && error.Problem.Contains("GadgetPartResponse", StringComparison.Ordinal))).IsTrue(); + } + + [Test] + public async Task Bind_Should_Refuse_Colliding_Route_Members_In_A_Container() + { + var document = await BindingTestHost.IngestAsync(GadgetScenario(spec => spec + .WithOperation("v2.gadget.part.get", path: "/api/gadget-part", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("GadgetPart"))))); + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.gadget.part", "v2.gadget.part.get"), + Curation(Groups("gadget", ClientGroup(clientName: "Gadgets", handleName: "GadgetClient", handleParameter: "gadgetID"))))); + + await Assert.That(exception.Errors.Any(static error => error.Category == BindingErrorCategory.Naming + && error.Problem.Contains("route member", StringComparison.Ordinal) + && error.Problem.Contains("GetPart", StringComparison.Ordinal))).IsTrue(); + } + + [Test] + public async Task Bind_Should_Refuse_Colliding_Client_Type_Names() + { + var document = await BindingTestHost.IngestAsync(GadgetScenario()); + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.gadget.part"), + Curation(Groups("gadget", ClientGroup(clientName: "Gadget", handleName: "GadgetClient", handleParameter: "gadgetID"))))); + + await Assert.That(exception.Errors.Any(static error => error.Category == BindingErrorCategory.Naming + && error.Problem.Contains("GadgetClient", StringComparison.Ordinal))).IsTrue(); + } + + [Test] + public async Task Bind_Should_Apply_A_Curated_Payload_Name_Override() + { + var document = await BindingTestHost.IngestAsync(GadgetScenario()); + var payloadNames = new Dictionary(StringComparer.Ordinal) + { + ["v2.gadget.part"] = "Component", + }; + + var plan = new BindingTestHost().Bind( + document, + Selection("v2.gadget.part"), + Curation( + Groups("gadget", ClientGroup(clientName: "Gadgets", handleName: "GadgetClient", handleParameter: "gadgetID")), + payloadNames)); + + var part = plan.Clients.Single(static client => client.Role == ClientRole.Handle).Operations.Single(); + await Assert.That(part.Envelope.PayloadName).IsEqualTo("Component"); + } + + [Test] + public async Task Bind_Should_Refuse_A_Payload_Name_Colliding_With_The_Response_Spine() + { + var document = await BindingTestHost.IngestAsync(StatusScenario()); + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.widget.status"), + Curation(Groups("widget", ClientGroup(clientName: "Widgets", handleName: null, handleParameter: null))))); + + await Assert.That(exception.Errors.Single(static error => error.Category == BindingErrorCategory.Naming).Problem) + .Contains("Status"); + } + + [Test] + public async Task Bind_Should_Accept_An_Override_That_Resolves_A_Reserved_Payload_Collision() + { + var document = await BindingTestHost.IngestAsync(StatusScenario()); + var payloadNames = new Dictionary(StringComparer.Ordinal) + { + ["v2.widget.status"] = "WidgetStatus", + }; + + var plan = new BindingTestHost().Bind( + document, + Selection("v2.widget.status"), + Curation(Groups("widget", ClientGroup(clientName: "Widgets", handleName: null, handleParameter: null)), payloadNames)); + + var status = plan.Clients.Single(static client => client.Role == ClientRole.Collection).Operations.Single(); + await Assert.That(status.Envelope.PayloadName).IsEqualTo("WidgetStatus"); + } + + [Test] + public async Task Bind_Should_Report_Failures_For_Every_Selected_Operation() + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("ItemInfo", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithOperation("v2.health.get", configure: operation => operation + .Parameter("limit", "query", schema => schema.Type("string")) + .Response(200, "application/json", schema => schema.Ref("ItemInfo"))) + .WithOperation("v2.health.probe", path: "/api/health-probe", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("ItemInfo")) + .Response(302, "application/json", schema => schema.Ref("ItemInfo"))))); + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.health.get", "v2.health.probe"), + Curation(Groups("health", RootGroup())))); + + var subjects = exception.Errors + .Where(static error => error.Category == BindingErrorCategory.Operation) + .Select(static error => error.Subject) + .ToArray(); + await Assert.That(subjects).Contains("v2.health.get"); + await Assert.That(subjects).Contains("v2.health.probe"); + } + + private static async Task AssertOperationRefusalAsync(SpecDocument document, string operationId, string expectedProblem) + { + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection(operationId), + Curation(Groups("health", RootGroup())))); + + await Assert.That(exception.Errors.Any(error => error.Category == BindingErrorCategory.Operation + && string.Equals(error.Subject, operationId, StringComparison.Ordinal) + && error.Problem.Contains(expectedProblem, StringComparison.Ordinal))).IsTrue(); + } + + private static SpecScenario GadgetScenario(Action? extend = null, bool parametersReversed = false) => + SpecScenario.Define(spec => + { + _ = spec + .WithSchema("GadgetPart", schema => schema.Type("object") + .Property("id", property => property.Type("string"), required: true)) + .WithSchema("GadgetMissingError", schema => schema.Type("object") + .Property("_tag", property => property.Type("string").Enum("GadgetMissingError"), required: true) + .Property("message", property => property.Type("string"), required: true)) + .WithOperation("v2.gadget.part", path: "/api/gadget/{gadgetID}/part/{partID}", configure: operation => + { + if (parametersReversed) + { + _ = operation.Parameter("partID", "path", schema => schema.Type("string"), required: true) + .Parameter("gadgetID", "path", schema => schema.Type("string"), required: true); + } + else + { + _ = operation.Parameter("gadgetID", "path", schema => schema.Type("string"), required: true) + .Parameter("partID", "path", schema => schema.Type("string"), required: true); + } + + _ = operation + .Summary("Get gadget part") + .Response(200, "application/json", schema => schema.Type("object") + .Property("data", property => property.Ref("GadgetPart"), required: true)) + .Response(404, "application/json", schema => schema.Ref("GadgetMissingError")); + }); + extend?.Invoke(spec); + }); + + private static SpecScenario StatusScenario() => + SpecScenario.Define(spec => spec + .WithSchema("WidgetState", schema => schema.Type("object") + .Property("value", property => property.Type("string"), required: true)) + .WithOperation("v2.widget.status", path: "/api/widget-status", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("WidgetState")))); +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs index 4d96acf41..f40aee093 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs @@ -4,6 +4,7 @@ using OpenCode.Sdk.Tools.Generator.Ingestion.Models; using OpenCode.Sdk.Tools.Tests.Support; using Testably.Abstractions; +using static OpenCode.Sdk.Tools.Tests.Support.BindingScenarioData; namespace OpenCode.Sdk.Tools.Tests.Generator.Binding; @@ -25,12 +26,11 @@ public async Task Bind_Should_Create_The_Selected_Pinned_Emit_Plan() var plan = new BindingTestHost().Bind(document, selection, curation); await Assert.That(plan.SelectedOperationIds.SequenceEqual(selection.OperationIds, StringComparer.Ordinal)).IsTrue(); - await Assert.That(plan.PendingOperations.Any(static operation => operation.Surface == SpecSurface.Modern)).IsTrue(); - await Assert.That(plan.PendingOperations.Any(static operation => operation.Surface == SpecSurface.Legacy)).IsTrue(); - await Assert.That(plan.Models.Any(static model => model.Name == "PromptFileAttachment")).IsTrue(); - await Assert.That(plan.Models.Any(static model => model.Name == "Config")).IsFalse(); + await Assert.That(plan.PendingOperations).IsNotEmpty(); + await Assert.That(plan.Models.Any(static model => model.Name == "PromptFileSourceUri")).IsTrue(); + await Assert.That(plan.Models.Any(static model => model.Name == "ConfigInfo")).IsFalse(); - var promptFile = plan.Models.OfType().Single(static model => model.Name == "PromptFileAttachment"); + var promptFile = plan.Models.OfType().Single(static model => model.Name == "PromptFileSourceUri"); var promptUri = promptFile.Properties.Single(static property => property.WireName == "uri").Type; await Assert.That(promptUri).IsTypeOf(); await Assert.That(((NamedTypeReferencePlan)promptUri).Name).IsEqualTo("Uri"); @@ -40,12 +40,21 @@ public async Task Bind_Should_Create_The_Selected_Pinned_Emit_Plan() await Assert.That(toolUri).IsTypeOf(); await Assert.That(((NamedTypeReferencePlan)toolUri).Name).IsEqualTo("Uri"); - var sessionMessage = plan.Unions.Single(static union => union.Name == "SessionMessage"); + var sessionMessage = plan.Unions.Single(static union => union.Name == "SessionMessageInfo"); await Assert.That(sessionMessage.MarkerWireName).IsEqualTo("type"); - await Assert.That(sessionMessage.Variants).Count().IsEqualTo(8); + await Assert.That(sessionMessage.Variants).Count().IsEqualTo(10); + + var compaction = plan.Unions.Single(static union => union.Name == "SessionMessageCompaction"); + await Assert.That(compaction.MarkerWireName).IsEqualTo("status"); + await Assert.That(compaction.BaseTypeName).IsEqualTo("SessionMessageInfo"); + await Assert.That(compaction.FixedMarker!.WireName).IsEqualTo("type"); + await Assert.That(compaction.FixedMarker.Value).IsEqualTo("compaction"); + await Assert.That(compaction.Variants).Count().IsEqualTo(3); + await Assert.That(plan.Unions.Any(static union => union.Name == "SessionMessageAssistantContent")).IsTrue(); await Assert.That(plan.Unions.Any(static union => union.Name == "SessionMessageToolState")).IsTrue(); - await Assert.That(plan.Unions.Any(static union => union.Name == "LlmToolContent")).IsTrue(); + await Assert.That(plan.Unions.Any(static union => union.Name == "PromptFileSource")).IsTrue(); + await Assert.That(plan.Unions.Any(static union => union.Name == "ToolContent")).IsTrue(); var errors = plan.Unions.Single(static union => union.Name == "OpenCodeError"); await Assert.That(errors.Variants.Select(static variant => variant.TypeName).Order(StringComparer.Ordinal) @@ -66,6 +75,12 @@ await Assert.That(first.Models.Select(static model => model.Name) await Assert.That(first.Unions.Select(static union => union.Name) .SequenceEqual(second.Unions.Select(static union => union.Name), StringComparer.Ordinal)).IsTrue(); await Assert.That(first.Registry.TypeNames.SequenceEqual(second.Registry.TypeNames, StringComparer.Ordinal)).IsTrue(); + await Assert.That(first.Clients.Select(static client => client.Name) + .SequenceEqual(second.Clients.Select(static client => client.Name), StringComparer.Ordinal)).IsTrue(); + await Assert.That(first.Clients.SelectMany(static client => client.Operations.Select(static operation => operation.MethodName)) + .SequenceEqual( + second.Clients.SelectMany(static client => client.Operations.Select(static operation => operation.MethodName)), + StringComparer.Ordinal)).IsTrue(); } [Test] @@ -109,28 +124,6 @@ await Assert.That(exception.Errors.Single(static error => error.Category == Bind .Contains("structural union"); } - [Test] - public async Task Bind_Should_Require_Selected_Envelope_Payload_Name() - { - var document = await IngestAsync(SpecScenario.Define(spec => spec - .WithOperation("v2.session.message", path: "/api/session/{sessionID}/message", configure: operation => operation - .Parameter("sessionID", "path", schema => schema.Type("string"), required: true) - .Response(200, "application/json", schema => schema.Type("object") - .Property("data", property => property.Type("string"), required: true))))); - var groups = new Dictionary(StringComparer.Ordinal) - { - ["session"] = ClientGroup(), - }; - - var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( - document, - Selection("v2.session.message"), - Curation(groups))); - - await Assert.That(exception.Errors.Single(static error => error.Category == BindingErrorCategory.Curation).Problem) - .Contains("payload name"); - } - [Test] public async Task Bind_Should_Refuse_Operation_Curation_For_Pending_Operation() { @@ -178,6 +171,149 @@ public async Task Bind_Should_Collapse_Duplicate_References_Into_Semantic_Type_P await Assert.That(((NamedTypeReferencePlan)((ListTypeReferencePlan)items).ElementType).Name).IsEqualTo("Shared"); } + [Test] + public async Task Bind_Should_Bind_Nested_Marked_Union_With_The_Discriminating_Marker() + { + var document = await IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("Alpha", schema => schema.Type("object") + .Property("type", property => property.Type("string").Enum("alpha"), required: true)) + .WithSchema("WrapOne", schema => schema.Type("object") + .Property("type", property => property.Type("string").Enum("wrap"), required: true) + .Property("status", property => property.Type("string").Enum("one"), required: true)) + .WithSchema("WrapTwo", schema => schema.Type("object") + .Property("type", property => property.Type("string").Enum("wrap"), required: true) + .Property("status", property => property.Type("string").Enum("two"), required: true)) + .WithSchema("Wrap", schema => schema.AnyOf(one => one.Ref("WrapOne"), two => two.Ref("WrapTwo"))) + .WithSchema("Outer", schema => schema.AnyOf(alpha => alpha.Ref("Alpha"), wrap => wrap.Ref("Wrap"))) + .WithOperation("v2.health.get", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("Outer"))))); + + var plan = new BindingTestHost().Bind( + document, + Selection("v2.health.get"), + Curation(new Dictionary(StringComparer.Ordinal) { ["health"] = RootGroup(), })); + + var outer = plan.Unions.Single(static union => union.Name == "Outer"); + await Assert.That(outer.MarkerWireName).IsEqualTo("type"); + await Assert.That(outer.Variants.Select(static variant => variant.Tag).Order(StringComparer.Ordinal) + .SequenceEqual(["alpha", "wrap"], StringComparer.Ordinal)).IsTrue(); + + var nested = plan.Unions.Single(static union => union.Name == "Wrap"); + await Assert.That(nested.MarkerWireName).IsEqualTo("status"); + await Assert.That(nested.BaseTypeName).IsEqualTo("Outer"); + await Assert.That(nested.FixedMarker!.WireName).IsEqualTo("type"); + await Assert.That(nested.FixedMarker.Value).IsEqualTo("wrap"); + } + + [Test] + public async Task Bind_Should_Refuse_Union_When_Branches_Share_No_Discriminating_Marker() + { + var document = await IngestAsync(SpecScenario.Define(spec => spec + .WithSchema("First", schema => schema.Type("object") + .Property("type", property => property.Type("string").Enum("same"), required: true)) + .WithSchema("Second", schema => schema.Type("object") + .Property("type", property => property.Type("string").Enum("same"), required: true)) + .WithSchema("Twin", schema => schema.AnyOf(first => first.Ref("First"), second => second.Ref("Second"))) + .WithOperation("v2.health.get", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("Twin"))))); + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.health.get"), + Curation(new Dictionary(StringComparer.Ordinal) { ["health"] = RootGroup(), }))); + + await Assert.That(exception.Errors.Single(static error => error.Category == BindingErrorCategory.Schema).Problem) + .Contains("no discriminating marker"); + } + + [Test] + public async Task Bind_Should_Refuse_HandleName_Without_HandleParameter() + { + var document = await IngestAsync(SpecScenario.Define(spec => spec + .WithOperation("v2.session.message", path: "/api/session/{sessionID}/message", configure: operation => operation + .Parameter("sessionID", "path", schema => schema.Type("string"), required: true) + .Response(200, "application/json", schema => schema.Type("object") + .Property("value", property => property.Type("string"), required: true))))); + var groups = new Dictionary(StringComparer.Ordinal) + { + ["session"] = ClientGroup(handleParameter: null), + }; + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.session.message"), + Curation(groups))); + + await Assert.That(exception.Errors.Single(static error => error.Category == BindingErrorCategory.Curation).Problem) + .Contains("together"); + } + + [Test] + public async Task Bind_Should_Refuse_HandleParameter_Without_HandleName() + { + var document = await IngestAsync(SpecScenario.Define(spec => spec + .WithOperation("v2.session.message", path: "/api/session/{sessionID}/message", configure: operation => operation + .Parameter("sessionID", "path", schema => schema.Type("string"), required: true) + .Response(200, "application/json", schema => schema.Type("object") + .Property("value", property => property.Type("string"), required: true))))); + var groups = new Dictionary(StringComparer.Ordinal) + { + ["session"] = ClientGroup(handleName: null), + }; + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.session.message"), + Curation(groups))); + + await Assert.That(exception.Errors.Single(static error => error.Category == BindingErrorCategory.Curation).Problem) + .Contains("together"); + } + + [Test] + public async Task Bind_Should_Refuse_HandleParameter_On_Root_Group() + { + var document = await IngestAsync(SpecScenario.Define(spec => spec.WithOperation("v2.health.get"))); + var groups = new Dictionary(StringComparer.Ordinal) + { + ["health"] = new GroupCuration + { + Placement = GroupPlacement.Root, + HandleParameter = "sessionID", + }, + }; + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.health.get"), + Curation(groups))); + + await Assert.That(exception.Errors.Any(static error => error.Category == BindingErrorCategory.Curation + && error.Problem.Contains("root group cannot declare", StringComparison.Ordinal))).IsTrue(); + } + + [Test] + public async Task Bind_Should_Require_HandleParameter_To_Name_A_Selected_Required_Path_Parameter() + { + var document = await IngestAsync(SpecScenario.Define(spec => spec + .WithOperation("v2.session.message", path: "/api/session/message/{messageID}", configure: operation => operation + .Parameter("messageID", "path", schema => schema.Type("string"), required: true) + .Response(200, "application/json", schema => schema.Type("object") + .Property("value", property => property.Type("string"), required: true))))); + var groups = new Dictionary(StringComparer.Ordinal) + { + ["session"] = ClientGroup(), + }; + + var exception = Assert.Throws(() => _ = new BindingTestHost().Bind( + document, + Selection("v2.session.message"), + Curation(groups))); + + await Assert.That(exception.Errors.Single(static error => error.Category == BindingErrorCategory.Curation).Problem) + .Contains("required path parameter"); + } + [Test] public async Task Bind_Should_Require_Curation_For_Every_Selected_Group() { @@ -232,23 +368,6 @@ private static async Task IngestAsync(SpecScenario scenario) return await new SpecIngestion(context.FileSystem).IngestAsync(context.SpecPath, CancellationToken.None); } - private static OperationSelection Selection(params string[] operationIds) => - new() - { - OperationIds = operationIds, - }; - - private static GenerationCuration Curation( - IReadOnlyDictionary groups, - IReadOnlyDictionary? envelopePayloadNames = null, - IReadOnlyList? propertyOverrides = null) => - new() - { - Groups = groups, - EnvelopePayloadNames = envelopePayloadNames ?? new Dictionary(StringComparer.Ordinal), - PropertyOverrides = propertyOverrides ?? [], - }; - private static PropertyOverride UriOverride(string schema, string property) => new() { @@ -257,18 +376,4 @@ private static PropertyOverride UriOverride(string schema, string property) => Type = PropertyOverrideType.Uri, Reason = "The fixture omits format: uri.", }; - - private static GroupCuration RootGroup() => - new() - { - Placement = GroupPlacement.Root, - }; - - private static GroupCuration ClientGroup() => - new() - { - Placement = GroupPlacement.Client, - ClientName = "Sessions", - HandleName = "SessionClient", - }; } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs new file mode 100644 index 000000000..acaaf492f --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs @@ -0,0 +1,17 @@ +using OpenCode.Sdk.Tools.Generator.Emission; +using OpenCode.Sdk.Tools.Tests.Support; + +namespace OpenCode.Sdk.Tools.Tests.Generator.Emission; + +public sealed class CallableSurfaceCompilationTests +{ + [Test] + public async Task Emit_Should_Produce_A_Compilable_Callable_Surface() + { + var sources = SourceEmitter.Emit(EmitterPlanFixture.Create()); + + var diagnostics = await GeneratedSourceCompiler.CompileWithSdkCoreAsync(sources); + + await Assert.That(diagnostics).IsEmpty(); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ClientEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ClientEmitterTests.cs new file mode 100644 index 000000000..02e9b7fb3 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ClientEmitterTests.cs @@ -0,0 +1,15 @@ +using OpenCode.Sdk.Tools.Generator.Emission; +using OpenCode.Sdk.Tools.Tests.Support; + +namespace OpenCode.Sdk.Tools.Tests.Generator.Emission; + +public sealed class ClientEmitterTests +{ + [Test] + public async Task Emit_Should_Produce_Virtual_Delegating_Clients() + { + var sources = ClientEmitter.Emit(EmitterPlanFixture.CreateClientPlans()); + + await Verify(EmitterSnapshot.Create(sources)); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/EnvelopeEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/EnvelopeEmitterTests.cs new file mode 100644 index 000000000..367e609cd --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/EnvelopeEmitterTests.cs @@ -0,0 +1,15 @@ +using OpenCode.Sdk.Tools.Generator.Emission; +using OpenCode.Sdk.Tools.Tests.Support; + +namespace OpenCode.Sdk.Tools.Tests.Generator.Emission; + +public sealed class EnvelopeEmitterTests +{ + [Test] + public async Task Emit_Should_Produce_Guarded_Response_Envelopes() + { + var sources = EnvelopeEmitter.Emit(EmitterPlanFixture.CreateClientPlans()); + + await Verify(EmitterSnapshot.Create(sources)); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ModelEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ModelEmitterTests.cs index 4d958ab09..0fa518e80 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ModelEmitterTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ModelEmitterTests.cs @@ -12,4 +12,13 @@ public async Task Emit_Should_Produce_Immutable_Annotated_Models() await Verify(EmitterSnapshot.Create(sources)); } + + [Test] + public async Task Emit_Should_Route_Optional_Nonnull_Collections_Through_The_Nullable_Input_Helper() + { + var source = EmitterSnapshot.Create(ModelEmitter.Emit(EmitterPlanFixture.CreateModelSnapshot())); + + await Assert.That(source).Contains("OptionalCollectionInput.Normalize"); + await Assert.That(source).DoesNotContain("value is null"); + } } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/OptionalCollectionInputEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/OptionalCollectionInputEmitterTests.cs new file mode 100644 index 000000000..2a2e6a5c5 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/OptionalCollectionInputEmitterTests.cs @@ -0,0 +1,15 @@ +using OpenCode.Sdk.Tools.Generator.Emission; +using OpenCode.Sdk.Tools.Tests.Support; + +namespace OpenCode.Sdk.Tools.Tests.Generator.Emission; + +public sealed class OptionalCollectionInputEmitterTests +{ + [Test] + public async Task Emit_Should_Produce_The_Nullable_Input_Normalizer() + { + var source = OptionalCollectionInputEmitter.Emit(); + + await Verify(EmitterSnapshot.Create([source])); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ResponseAdapterEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ResponseAdapterEmitterTests.cs new file mode 100644 index 000000000..3d347d898 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ResponseAdapterEmitterTests.cs @@ -0,0 +1,15 @@ +using OpenCode.Sdk.Tools.Generator.Emission; +using OpenCode.Sdk.Tools.Tests.Support; + +namespace OpenCode.Sdk.Tools.Tests.Generator.Emission; + +public sealed class ResponseAdapterEmitterTests +{ + [Test] + public async Task Emit_Should_Produce_Status_Mapped_Adapters() + { + var sources = ResponseAdapterEmitter.Emit(EmitterPlanFixture.CreateClientPlans()); + + await Verify(EmitterSnapshot.Create(sources)); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/RoutesEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/RoutesEmitterTests.cs new file mode 100644 index 000000000..94a65cf14 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/RoutesEmitterTests.cs @@ -0,0 +1,15 @@ +using OpenCode.Sdk.Tools.Generator.Emission; +using OpenCode.Sdk.Tools.Tests.Support; + +namespace OpenCode.Sdk.Tools.Tests.Generator.Emission; + +public sealed class RoutesEmitterTests +{ + [Test] + public async Task Emit_Should_Produce_Escaped_Route_Builders() + { + var source = RoutesEmitter.Emit(EmitterPlanFixture.CreateClientPlans()); + + await Verify(EmitterSnapshot.Create([source])); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/SourceEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/SourceEmitterTests.cs index 714f74112..66d674def 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/SourceEmitterTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/SourceEmitterTests.cs @@ -20,22 +20,12 @@ await Assert.That(first.Select(static source => source.Utf8Source.ToArray()) .All(static equal => equal)).IsTrue(); } - [Test] - public async Task Emit_Should_Produce_Compilable_Source() - { - var sources = SourceEmitter.Emit(EmitterPlanFixture.Create()); - - var diagnostics = GeneratedSourceCompiler.Compile(sources); - - await Assert.That(diagnostics).IsEmpty(); - } - [Test] public async Task Emit_Should_Produce_Compilable_Source_For_The_Selected_Pin() { var plan = await new BindingTestHost().BindPinnedAsync(); - var diagnostics = GeneratedSourceCompiler.Compile(SourceEmitter.Emit(plan)); + var diagnostics = await GeneratedSourceCompiler.CompileWithSdkCoreAsync(SourceEmitter.Emit(plan)); await Assert.That(diagnostics).IsEmpty(); } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/OperationProjectorTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/OperationProjectorTests.cs index e796c0296..0fde1ce98 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/OperationProjectorTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/OperationProjectorTests.cs @@ -6,26 +6,36 @@ namespace OpenCode.Sdk.Tools.Tests.Generator.Ingestion; public sealed class OperationProjectorTests { [Test] - public async Task Project_Should_Preserve_Order_And_Derive_Surface_And_Segments() + public async Task Project_Should_Preserve_Order_And_Derive_Segments() { var host = new OperationProjectionTestHost(); var scenario = SpecScenario.Define(spec => spec .WithOperation("v2.session.list", path: "/api/session") - .WithOperation("session.list", path: "/api/v2/session")); + .WithOperation("v2.agent.list", path: "/api/agent")); var result = await host.ProjectAsync(scenario); await Assert.That(result.Operations).Count().IsEqualTo(2); await Assert.That(result.Operations[0].OperationId).IsEqualTo("v2.session.list"); - await Assert.That(result.Operations[0].Surface).IsEqualTo(SpecSurface.Modern); await Assert.That(result.Operations[0].Segments[0]).IsEqualTo("session"); await Assert.That(result.Operations[0].Segments[1]).IsEqualTo("list"); - await Assert.That(result.Operations[1].OperationId).IsEqualTo("session.list"); - await Assert.That(result.Operations[1].Surface).IsEqualTo(SpecSurface.Legacy); - await Assert.That(result.Operations[1].Segments[0]).IsEqualTo("session"); + await Assert.That(result.Operations[1].OperationId).IsEqualTo("v2.agent.list"); + await Assert.That(result.Operations[1].Segments[0]).IsEqualTo("agent"); await Assert.That(result.Operations[1].Segments[1]).IsEqualTo("list"); } + [Test] + public async Task Project_Should_Refuse_Operation_Without_The_Protocol_Prefix() + { + var host = new OperationProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithOperation("session.list", path: "/api/session")); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("protocol prefix"); + await Assert.That(ex.Message).Contains("session.list"); + } + [Test] public async Task Project_Should_Preserve_Deep_Operation_Segments() { @@ -80,7 +90,7 @@ public async Task Project_Should_Record_WebSocket_Flag() public async Task Project_Should_Record_Deprecation_And_Documentation() { var host = new OperationProjectionTestHost(); - var scenario = SpecScenario.Define(spec => spec.WithOperation("session.old", configure: operation => operation + var scenario = SpecScenario.Define(spec => spec.WithOperation("v2.session.old", configure: operation => operation .Deprecated() .Summary("Old operation") .Description("Use the replacement."))); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PinnedSpecSmokeTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PinnedSpecSmokeTests.cs index dbcb08c84..93263a472 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PinnedSpecSmokeTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PinnedSpecSmokeTests.cs @@ -12,12 +12,13 @@ namespace OpenCode.Sdk.Tools.Tests.Generator.Ingestion; public sealed class PinnedSpecSmokeTests { [Test] - public async Task Ingest_Should_Absorb_The_Pinned_Spec_With_Both_Surfaces() + public async Task Ingest_Should_Absorb_The_Pinned_Spec() { var document = await IngestPinnedSpecAsync(); - await Assert.That(document.Operations.Any(static operation => operation.Surface == SpecSurface.Modern)).IsTrue(); - await Assert.That(document.Operations.Any(static operation => operation.Surface == SpecSurface.Legacy)).IsTrue(); + await Assert.That(document.Operations).IsNotEmpty(); + await Assert.That(document.Operations.All(static operation => + operation.OperationId.StartsWith("v2.", StringComparison.Ordinal))).IsTrue(); } [Test] @@ -25,27 +26,38 @@ public async Task Ingest_Should_Flag_Stream_Wildcard_And_WebSocket_Operations() { var document = await IngestPinnedSpecAsync(); - var durableStream = document.Operations.Single(static operation => operation.OperationId == "v2.session.events"); - await Assert.That(durableStream.IsSse).IsTrue(); - await Assert.That(durableStream.Responses.Single(static response => response.IsSse).EffectStreamJson).IsNotNull(); - await Assert.That(document.Operations.Single(static operation => operation.OperationId == "v2.fs.read").HasWildcardPath).IsTrue(); - await Assert.That(document.Operations.Single(static operation => operation.OperationId == "v2.pty.connect").IsWebSocket).IsTrue(); + var sessionLog = document.Operations.Single(static operation => operation.OperationId == "v2.session.log"); + await Assert.That(sessionLog.IsSse).IsTrue(); + await Assert.That(sessionLog.Responses.Single(static response => response.IsSse).EffectStreamJson).IsNotNull(); + await Assert.That(document.Operations.Single(static operation => operation.OperationId == "v2.event.subscribe").IsSse) + .IsTrue(); + await Assert.That(document.Operations.Single(static operation => operation.OperationId == "v2.fs.read").HasWildcardPath) + .IsTrue(); + await Assert.That(document.Operations.Single(static operation => operation.OperationId == "v2.pty.connect").IsWebSocket) + .IsTrue(); } [Test] - public async Task Ingest_Should_Classify_Marked_And_Structural_Union_Landmarks() + public async Task Ingest_Should_Classify_Marked_Nested_And_Structural_Union_Landmarks() { var document = await IngestPinnedSpecAsync(); - var durableEvent = (UnionNode)document.Schemas["SessionDurableEvent"]; + var durableEvent = (UnionNode)document.Schemas["Session.Event.Durable"]; await Assert.That(durableEvent.Keyword).IsEqualTo(UnionKeyword.OneOf); await Assert.That(durableEvent.Classification).IsEqualTo(UnionClassification.Marked); - var formatter = (UnionNode)document.Schemas["Config#/properties/formatter"]; + var message = (UnionNode)document.Schemas["Session.Message.Info"]; + await Assert.That(message.Classification).IsEqualTo(UnionClassification.Marked); + + var compaction = (UnionNode)document.Schemas["Session.Message.Compaction"]; + await Assert.That(compaction.Classification).IsEqualTo(UnionClassification.Marked); + + var formatter = (UnionNode)document.Schemas["Config.Info#/properties/formatter"]; await Assert.That(formatter.Classification).IsEqualTo(UnionClassification.Structural); - var pluginItems = (UnionNode)document.Schemas["Config#/properties/plugin/items"]; - await Assert.That(pluginItems.Branches.OfType().Single().Items).Count().IsEqualTo(2); + var fields = (ArrayNode)document.Schemas["Form.Fields"]; + await Assert.That(fields.Item).IsTypeOf(); + await Assert.That(((RefNode)fields.Item).Target).IsEqualTo("Form.Field"); } [Test] @@ -53,12 +65,12 @@ public async Task Ingest_Should_Project_Special_Number_And_Unrestricted_Landmark { var document = await IngestPinnedSpecAsync(); - var workspace = (ObjectNode)document.Schemas["Workspace"]; - await Assert.That(workspace.Properties.Single(static property => property.Name == "timeUsed").Schema) + var shell = (ObjectNode)document.Schemas["Session.Message.Shell"]; + await Assert.That(shell.Properties.Single(static property => property.Name == "exit").Schema) .IsTypeOf(); - var assistantMessage = (ObjectNode)document.Schemas["AssistantMessage"]; - await Assert.That(assistantMessage.Properties.Single(static property => property.Name == "structured").Schema) + var instructionEntry = (ObjectNode)document.Schemas["InstructionEntry.Info"]; + await Assert.That(instructionEntry.Properties.Single(static property => property.Name == "value").Schema) .IsTypeOf(); } @@ -67,8 +79,8 @@ public async Task Ingest_Should_Classify_Both_Error_Styles() { var document = await IngestPinnedSpecAsync(); - await Assert.That(((ObjectNode)document.Schemas["MoveSessionError"]).ErrorStyle).IsEqualTo(ErrorStyle.NameData); - await Assert.That(((ObjectNode)document.Schemas["effect_HttpApiError_BadRequest"]).ErrorStyle).IsEqualTo(ErrorStyle.EffectTag); + await Assert.That(((ObjectNode)document.Schemas["ProjectCopyError"]).ErrorStyle).IsEqualTo(ErrorStyle.NameData); + await Assert.That(((ObjectNode)document.Schemas["UnauthorizedError"]).ErrorStyle).IsEqualTo(ErrorStyle.EffectTag); } [Test] @@ -77,8 +89,8 @@ public async Task Ingest_Should_Classify_Envelope_Shapes() var document = await IngestPinnedSpecAsync(); await Assert.That(EnvelopeOf(document, "v2.session.list")).IsEqualTo(SpecEnvelopeShape.CursorData); - await Assert.That(EnvelopeOf(document, "v2.session.history")).IsEqualTo(SpecEnvelopeShape.DataHasMore); await Assert.That(EnvelopeOf(document, "v2.agent.list")).IsEqualTo(SpecEnvelopeShape.DataLocation); + await Assert.That(EnvelopeOf(document, "v2.session.message")).IsEqualTo(SpecEnvelopeShape.Data); } [Test] diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PrefixItemsAdapterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PrefixItemsAdapterTests.cs index 9986e3e56..d55f681a9 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PrefixItemsAdapterTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PrefixItemsAdapterTests.cs @@ -42,7 +42,61 @@ public async Task Project_Should_Refuse_When_Tuple_Arity_Conflicts_With_MinItems } [Test] - public async Task Project_Should_Refuse_When_Items_And_PrefixItems_Are_Combined() + public async Task Project_Should_Produce_Array_When_Single_Prefix_Element_Shares_The_Items_Target() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec + .WithSchema("Field", schema => schema.Type("string")) + .WithSchema("Fields", schema => schema + .Type("array") + .PrefixItems(item => item.Ref("Field")) + .MinItems(1) + .Items(item => item.Ref("Field")))); + + var result = await host.ProjectAsync(scenario); + + var array = (ArrayNode)result.Schemas["Fields"]; + await Assert.That(((RefNode)array.Item).Target).IsEqualTo("Field"); + } + + [Test] + public async Task Project_Should_Refuse_When_Prefix_Element_Differs_From_The_Items_Target() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec + .WithSchema("Field", schema => schema.Type("string")) + .WithSchema("Other", schema => schema.Type("string")) + .WithSchema("Bad", schema => schema + .Type("array") + .PrefixItems(item => item.Ref("Other")) + .MinItems(1) + .Items(item => item.Ref("Field")))); + + var exception = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(exception.Message).Contains("prefixItems"); + await Assert.That(exception.Message).Contains("$ref"); + } + + [Test] + public async Task Project_Should_Refuse_When_Items_Are_Paired_With_Multiple_Prefix_Elements() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec + .WithSchema("Field", schema => schema.Type("string")) + .WithSchema("Bad", schema => schema + .Type("array") + .PrefixItems(item => item.Ref("Field"), item => item.Ref("Field")) + .MinItems(2) + .Items(item => item.Ref("Field")))); + + var exception = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(exception.Message).Contains("single prefix element"); + } + + [Test] + public async Task Project_Should_Refuse_When_Combined_Items_And_PrefixItems_Are_Inline() { var host = new SchemaProjectionTestHost(); var scenario = SpecScenario.Define(spec => spec.WithConfigPluginTuple(tuple => tuple diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PrefixItemsTripwireTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PrefixItemsTripwireTests.cs index 7fa4eff96..aa5f8bce9 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PrefixItemsTripwireTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/PrefixItemsTripwireTests.cs @@ -12,15 +12,14 @@ public async Task Reader_Should_Retain_PrefixItems_As_Unrecognized_Keyword_At_Th // Library-upgrade tripwire: the prefixItems adapter reads the raw keyword the pinned // Microsoft.OpenApi retains as unrecognized. A newer library that types prefixItems // would leave UnrecognizedKeywords empty, blind the adapter, and silently degrade the - // Config.plugin tuple — this test must fail loudly before that can ship. + // Form.Fields non-empty array — this test must fail loudly before that can ship. var fileSystem = new RealFileSystem(); var specPath = fileSystem.Path.Combine(AppContext.BaseDirectory, "Fixtures", "openapi.json"); var errors = new IngestionErrorCollector(); var loaded = await new SpecReader(fileSystem).LoadAsync(specPath, errors, CancellationToken.None); - var plugin = (OpenApiSchema)loaded.Document.Components!.Schemas!["Config"].Properties!["plugin"]; - var tupleBranch = (OpenApiSchema)plugin.Items!.AnyOf![1]; - await Assert.That(tupleBranch.UnrecognizedKeywords!.Keys).Contains("prefixItems"); + var fields = (OpenApiSchema)loaded.Document.Components!.Schemas!["Form.Fields"]; + await Assert.That(fields.UnrecognizedKeywords!.Keys).Contains("prefixItems"); } } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/SchemaProjectorTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/SchemaProjectorTests.cs index 537a8dba5..5e69fae05 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/SchemaProjectorTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/SchemaProjectorTests.cs @@ -182,15 +182,15 @@ public async Task Project_Should_Refuse_When_Ref_Target_Is_Missing() } [Test] - public async Task Project_Should_Refuse_When_Array_Has_No_Items() + public async Task Project_Should_Produce_Array_Of_Any_Value_When_Items_Are_Absent() { var host = new SchemaProjectionTestHost(); - var scenario = SpecScenario.Define(spec => spec.WithSchema("Bad", schema => schema.Type("array"))); + var scenario = SpecScenario.Define(spec => spec.WithSchema("Value", schema => schema.Type("array"))); - var ex = await host.ProjectExpectingRefusalAsync(scenario); + var result = await host.ProjectAsync(scenario); - await Assert.That(ex.Message).Contains("items"); - await Assert.That(ex.Message).Contains("Bad"); + await Assert.That(result.Schemas["Value"]).IsTypeOf(); + await Assert.That(((ArrayNode)result.Schemas["Value"]).Item).IsTypeOf(); } [Test] @@ -240,13 +240,15 @@ public async Task Project_Should_Batch_Errors_From_Multiple_Schemas() { var host = new SchemaProjectionTestHost(); var scenario = SpecScenario.Define(spec => spec - .WithSchema("BadArray", schema => schema.Type("array")) + .WithSchema("BadAllOf", schema => schema + .Type("string") + .AllOf(first => first.Raw("pattern", "\"^a\""), second => second.Raw("minLength", "1"))) .WithSchema("BadKeyword", schema => schema.Raw("madeUpKeyword", "true"))); var ex = await host.ProjectExpectingRefusalAsync(scenario); await Assert.That(ex.Errors).Count().IsEqualTo(2); - await Assert.That(ex.Message).Contains("BadArray"); + await Assert.That(ex.Message).Contains("BadAllOf"); await Assert.That(ex.Message).Contains("BadKeyword"); } @@ -270,4 +272,189 @@ public async Task Project_Should_Refuse_When_Promoted_Graph_Key_Collides() await Assert.That(ex!.Message).Contains("collision"); await Assert.That(ex.Message).Contains("Shared#/value"); } + + [Test] + public async Task Project_Should_Unwrap_Validation_Only_AllOf_Wrapper() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Value", + schema => schema.Type("integer").AllOf(element => element.Raw("exclusiveMinimum", "0")))); + + var result = await host.ProjectAsync(scenario); + + await Assert.That(result.Schemas["Value"]).IsTypeOf(); + await Assert.That(((PrimitiveNode)result.Schemas["Value"]).Kind).IsEqualTo(PrimitiveKind.Integer); + } + + [Test] + public async Task Project_Should_Unwrap_Validation_Only_AllOf_Wrapper_When_Annotated() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Value", + schema => schema + .Type("string") + .AllOf(element => element.Raw("pattern", "\"^ses_\"").Description("session identifier")))); + + var result = await host.ProjectAsync(scenario); + + await Assert.That(result.Schemas["Value"]).IsTypeOf(); + await Assert.That(((PrimitiveNode)result.Schemas["Value"]).Kind).IsEqualTo(PrimitiveKind.String); + } + + [Test] + public async Task Project_Should_Refuse_AllOf_When_It_Has_Multiple_Elements() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Bad", + schema => schema + .Type("string") + .AllOf(first => first.Raw("pattern", "\"^a\""), second => second.Raw("minLength", "1")))); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("allOf"); + await Assert.That(ex.Message).Contains("Bad"); + } + + [Test] + public async Task Project_Should_Refuse_AllOf_Wrapper_When_Element_Has_Structural_Member() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Bad", + schema => schema.Type("string").AllOf(element => element.Type("string")))); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("allOf"); + } + + [Test] + public async Task Project_Should_Refuse_AllOf_Wrapper_When_Element_Has_Format() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Bad", + schema => schema.Type("string").AllOf(element => element.Format("uuid")))); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("allOf"); + } + + [Test] + public async Task Project_Should_Refuse_AllOf_Wrapper_When_Element_Has_Unrecognized_Keyword() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Bad", + schema => schema.Type("string").AllOf(element => element.Raw("customKeyword", "true")))); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("allOf"); + } + + [Test] + public async Task Project_Should_Refuse_AllOf_Wrapper_When_Element_Is_A_Reference() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec + .WithSchema("Other", schema => schema.Type("string")) + .WithSchema("Bad", schema => schema.Type("string").AllOf(element => element.Ref("Other")))); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("allOf"); + } + + [Test] + public async Task Project_Should_Refuse_AllOf_Wrapper_When_Element_Carries_An_Applicator() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Bad", + schema => schema.Type("string").AllOf(element => element.Raw("not", "{\"type\":\"number\"}")))); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("allOf"); + } + + [Test] + public async Task Project_Should_Produce_Number_Literal_From_Single_Value_Enum() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Value", + schema => schema.Type("number").Raw("enum", "[1]"))); + + var result = await host.ProjectAsync(scenario); + + var literal = (LiteralNode)result.Schemas["Value"]; + await Assert.That(literal.Kind).IsEqualTo(LiteralKind.Number); + await Assert.That(literal.Value).IsEqualTo("1"); + await Assert.That(literal.Dialect).IsEqualTo(LiteralDialect.SingleValueEnum); + } + + [Test] + public async Task Project_Should_Produce_Number_Literal_From_Single_Value_Integer_Enum() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Value", + schema => schema.Type("integer").Raw("enum", "[3]"))); + + var result = await host.ProjectAsync(scenario); + + var literal = (LiteralNode)result.Schemas["Value"]; + await Assert.That(literal.Kind).IsEqualTo(LiteralKind.Number); + await Assert.That(literal.Value).IsEqualTo("3"); + } + + [Test] + public async Task Project_Should_Refuse_Number_Enum_When_It_Has_Multiple_Values() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Bad", + schema => schema.Type("number").Raw("enum", "[1, 2]"))); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("enum"); + await Assert.That(ex.Message).Contains("Bad"); + } + + [Test] + public async Task Project_Should_Refuse_Number_Enum_When_Its_Value_Is_Not_A_Number() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Bad", + schema => schema.Type("number").Raw("enum", "[\"one\"]"))); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("must contain a number"); + } + + [Test] + public async Task Project_Should_Refuse_AllOf_When_Host_Carries_Other_Constraints() + { + var host = new SchemaProjectionTestHost(); + var scenario = SpecScenario.Define(spec => spec.WithSchema( + "Bad", + schema => schema + .Type("object") + .Property("value", property => property.Type("string")) + .AllOf(element => element.Raw("pattern", "\"^a\"")))); + + var ex = await host.ProjectExpectingRefusalAsync(scenario); + + await Assert.That(ex.Message).Contains("allOf"); + } } diff --git a/tests/OpenCode.Sdk.Tools.Tests/OpenCode.Sdk.Tools.Tests.csproj b/tests/OpenCode.Sdk.Tools.Tests/OpenCode.Sdk.Tools.Tests.csproj index 78646c8ce..9a32364d2 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/OpenCode.Sdk.Tools.Tests.csproj +++ b/tests/OpenCode.Sdk.Tools.Tests/OpenCode.Sdk.Tools.Tests.csproj @@ -27,6 +27,12 @@ + + diff --git a/tests/OpenCode.Sdk.Tools.Tests/Output/GenerationWriterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Output/GenerationWriterTests.cs index a32711e12..cc4abd832 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Output/GenerationWriterTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Output/GenerationWriterTests.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using OpenCode.Sdk.Tools.Generator.Emission; using OpenCode.Sdk.Tools.Output; using OpenCode.Sdk.Tools.Tests.Support; using Testably.Abstractions.Testing; @@ -102,7 +103,7 @@ public async Task WriteAsync_Should_Delete_Only_Stale_Manifest_Files() var manifest = JsonSerializer.Serialize(new GenerationManifest { Files = ["Models/Stale.cs"], }); fileSystem.Initialize().With( new FileDescription(GenerationTestData.ManifestPath, manifest), - new FileDescription(stalePath, "stale"), + new FileDescription(stalePath, GenerationTestData.OwnedContent("stale")), new FileDescription(unrelatedPath, "manual")); var writer = new GenerationWriter(fileSystem, new RecordingProjectFormatter(fileSystem)); @@ -172,7 +173,7 @@ public async Task WriteAsync_Should_Report_And_Repair_Verify_Drift() verify: false, CancellationToken.None); var outputPath = fileSystem.Path.Combine(GenerationTestData.OutputRoot, relativePath); - await fileSystem.File.WriteAllTextAsync(outputPath, "drift\n", CancellationToken.None); + await fileSystem.File.WriteAllTextAsync(outputPath, GenerationTestData.OwnedContent("drift\n"), CancellationToken.None); var result = await writer.WriteAsync( GenerationTestData.OutputRoot, @@ -185,7 +186,121 @@ public async Task WriteAsync_Should_Report_And_Repair_Verify_Drift() await Assert.That(result.HasChanges).IsTrue(); await Assert.That(result.ChangedPaths).Contains(relativePath); var repaired = await fileSystem.File.ReadAllTextAsync(outputPath, CancellationToken.None); - await Assert.That(repaired).IsEqualTo("expected\n"); + await Assert.That(repaired).IsEqualTo(GenerationTestData.OwnedContent("expected\n")); + } + + [Test] + public async Task WriteAsync_Should_Admit_Root_And_Adapter_Sources() + { + var fileSystem = GenerationTestData.CreateFileSystem(); + var writer = new GenerationWriter(fileSystem, new RecordingProjectFormatter(fileSystem)); + + var result = await writer.WriteAsync( + GenerationTestData.OutputRoot, + GenerationTestData.ProjectPath, + [ + GenerationTestData.Source("ExampleClient.cs", "client"), + GenerationTestData.Source("Internal/ResponseAdapters/ExampleAdapter.cs", "adapter"), + ], + partialMarkerContent: null, + verify: false, + CancellationToken.None); + + await Assert.That(result.CreatedPaths).Contains("ExampleClient.cs"); + await Assert.That(result.CreatedPaths).Contains("Internal/ResponseAdapters/ExampleAdapter.cs"); + } + + [Test] + public async Task WriteAsync_Should_Refuse_Overwriting_An_Unmanifested_File() + { + var fileSystem = GenerationTestData.CreateFileSystem(); + fileSystem.Initialize().With(new FileDescription("src/OpenCode.Sdk/ExampleClient.cs", "hand-written")); + var writer = new GenerationWriter(fileSystem, new RecordingProjectFormatter(fileSystem)); + + var exception = await Assert + .That(async () => _ = await writer.WriteAsync( + GenerationTestData.OutputRoot, + GenerationTestData.ProjectPath, + [GenerationTestData.Source("ExampleClient.cs", "generated")], + partialMarkerContent: null, + verify: false, + CancellationToken.None)) + .Throws(); + + await Assert.That(exception!.Message).Contains("unmanifested"); + var untouched = await fileSystem.File.ReadAllTextAsync("src/OpenCode.Sdk/ExampleClient.cs", CancellationToken.None); + await Assert.That(untouched).IsEqualTo("hand-written"); + } + + [Test] + public async Task WriteAsync_Should_Refuse_Overwriting_A_Manifest_Entry_Without_Provenance() + { + var fileSystem = GenerationTestData.CreateFileSystem(); + var manifest = JsonSerializer.Serialize(new GenerationManifest { Files = ["Models/Example.cs"], }); + fileSystem.Initialize().With( + new FileDescription(GenerationTestData.ManifestPath, manifest), + new FileDescription("src/OpenCode.Sdk/Models/Example.cs", "hand-edited without a header")); + var writer = new GenerationWriter(fileSystem, new RecordingProjectFormatter(fileSystem)); + + var exception = await Assert + .That(async () => _ = await writer.WriteAsync( + GenerationTestData.OutputRoot, + GenerationTestData.ProjectPath, + [GenerationTestData.Source("Models/Example.cs", "generated")], + partialMarkerContent: null, + verify: false, + CancellationToken.None)) + .Throws(); + + await Assert.That(exception!.Message).Contains("provenance"); + } + + [Test] + public async Task WriteAsync_Should_Refuse_Deleting_A_Manifest_Entry_Without_Provenance() + { + var fileSystem = GenerationTestData.CreateFileSystem(); + var manifest = JsonSerializer.Serialize(new GenerationManifest { Files = ["Models/Stale.cs"], }); + fileSystem.Initialize().With( + new FileDescription(GenerationTestData.ManifestPath, manifest), + new FileDescription("src/OpenCode.Sdk/Models/Stale.cs", "hand-edited without a header")); + var writer = new GenerationWriter(fileSystem, new RecordingProjectFormatter(fileSystem)); + + var exception = await Assert + .That(async () => _ = await writer.WriteAsync( + GenerationTestData.OutputRoot, + GenerationTestData.ProjectPath, + [GenerationTestData.Source("Models/Fresh.cs", "generated")], + partialMarkerContent: null, + verify: false, + CancellationToken.None)) + .Throws(); + + await Assert.That(exception!.Message).Contains("provenance"); + await Assert.That(fileSystem.File.Exists("src/OpenCode.Sdk/Models/Stale.cs")).IsTrue(); + } + + [Test] + public async Task WriteAsync_Should_Refuse_Headerless_Generated_Sources() + { + var fileSystem = GenerationTestData.CreateFileSystem(); + var writer = new GenerationWriter(fileSystem, new RecordingProjectFormatter(fileSystem)); + var headerless = new GeneratedSource + { + RelativePath = "Models/Example.cs", + Utf8Source = "no header"u8.ToArray(), + }; + + var exception = await Assert + .That(async () => _ = await writer.WriteAsync( + GenerationTestData.OutputRoot, + GenerationTestData.ProjectPath, + [headerless], + partialMarkerContent: null, + verify: false, + CancellationToken.None)) + .Throws(); + + await Assert.That(exception?.Message).Contains("provenance header"); } [Test] @@ -198,7 +313,7 @@ public async Task WriteAsync_Should_Create_And_Remove_The_Partial_Marker() GenerationTestData.OutputRoot, GenerationTestData.ProjectPath, [], - "Pending modern operations: 1.\nPending legacy operations: 2.\n", + "Pending operations: 3.\n", verify: false, CancellationToken.None); var removal = await writer.WriteAsync( diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ClientEmitterTests.Emit_Should_Produce_Virtual_Delegating_Clients.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ClientEmitterTests.Emit_Should_Produce_Virtual_Delegating_Clients.verified.txt new file mode 100644 index 000000000..2029cfe94 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ClientEmitterTests.Emit_Should_Produce_Virtual_Delegating_Clients.verified.txt @@ -0,0 +1,204 @@ +// File: OpenCodeClient.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using OpenCode.Sdk.Internal; +using OpenCode.Sdk.Internal.ResponseAdapters; + +namespace OpenCode.Sdk; +/// +/// The opencode API client and composition root. +/// +public class OpenCodeClient : IDisposable +{ + private readonly Pipeline? _pipeline; + private readonly WidgetsClient? _widgets; + /// + /// Initializes a client that owns its connection to the endpoint. + /// + /// The absolute HTTP or HTTPS server endpoint. + public OpenCodeClient(Uri endpoint) : this(endpoint, options: null) + { + } + + /// + /// Initializes a client that owns its connection to the endpoint. + /// + /// The absolute HTTP or HTTPS server endpoint. + /// The client options; its endpoint must stay unset on this path. + public OpenCodeClient(Uri endpoint, OpenCodeClientOptions? options) + { + _pipeline = Pipeline.Create(endpoint, options, new SystemEnvironmentProvider()); + _widgets = new WidgetsClient(_pipeline); + } + + /// + /// Initializes a client over a caller-owned HttpClient; the SDK never disposes it. + /// + /// The caller-owned HTTP client. + /// The client options; the endpoint is required on this path. + public OpenCodeClient(HttpClient httpClient, OpenCodeClientOptions options) + { + _pipeline = Pipeline.Create(httpClient, options, new SystemEnvironmentProvider()); + _widgets = new WidgetsClient(_pipeline); + } + + /// + /// Initializes a mocking instance; members invoked without an override throw an instructive failure. + /// + protected OpenCodeClient() + { + } + + /// + /// Releases the owned transport resources. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the owned transport resources. + /// + /// Whether managed state should be released. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _pipeline?.Dispose(); + } + } + + /// + /// Gets the 'Widgets' collection client. + /// + public virtual WidgetsClient Widgets => _widgets ?? throw MockSeam.CreateError("OpenCodeClient", "Widgets"); + private Pipeline Pipeline => _pipeline ?? throw MockSeam.CreateError("OpenCodeClient", "Pipeline"); + + /// + /// Check example ping + /// + /// The per-call options. + /// The cancellation token. + /// The 'PingResponse' envelope. + /// The API returned an error status (declared: 400) and NoThrow was not selected. + /// The server could not be reached or returned a malformed success body. + public virtual Task GetPingAsync(OpenCodeRequestOptions? options = null, CancellationToken cancellationToken = default) + { + return Pipeline.ExecuteAsync(HttpMethod.Get, OpenCodeRoutes.Ping.Get, PingResponseAdapter.Instance, options, cancellationToken); + } +} + +// File: WidgetClient.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using OpenCode.Sdk.Internal; +using OpenCode.Sdk.Internal.ResponseAdapters; + +namespace OpenCode.Sdk; +/// +/// A bound 'WidgetClient' handle; it holds an immutable identifier and the shared pipeline. +/// +public class WidgetClient +{ + private readonly Pipeline? _pipeline; + private readonly string? _widgetId; + internal WidgetClient(Pipeline pipeline, string widgetId) + { + ArgumentNullException.ThrowIfNull(pipeline); + ArgumentException.ThrowIfNullOrEmpty(widgetId); + _pipeline = pipeline; + _widgetId = widgetId; + } + + /// + /// Initializes a mocking instance; members invoked without an override throw an instructive failure. + /// + protected WidgetClient() + { + } + + private Pipeline Pipeline => _pipeline ?? throw MockSeam.CreateError("WidgetClient", "Pipeline"); + private string WidgetId => _widgetId ?? throw MockSeam.CreateError("WidgetClient", "WidgetId"); + + /// + /// Get one widget item + /// + /// The 'itemID' route value. + /// The per-call options. + /// The cancellation token. + /// The 'WidgetItemResponse' envelope. + /// The API returned an error status (declared: 400, 404) and NoThrow was not selected. + /// The server could not be reached or returned a malformed success body. + public virtual Task GetItemAsync(string itemId, OpenCodeRequestOptions? options = null, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(itemId); + return Pipeline.ExecuteAsync(HttpMethod.Get, OpenCodeRoutes.Widgets.GetItem(WidgetId, itemId), WidgetItemResponseAdapter.Instance, options, cancellationToken); + } +} + +// File: WidgetsClient.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using OpenCode.Sdk.Internal; +using OpenCode.Sdk.Internal.ResponseAdapters; + +namespace OpenCode.Sdk; +/// +/// The 'WidgetsClient' collection client. +/// +public class WidgetsClient +{ + private readonly Pipeline? _pipeline; + internal WidgetsClient(Pipeline pipeline) + { + ArgumentNullException.ThrowIfNull(pipeline); + _pipeline = pipeline; + } + + /// + /// Initializes a mocking instance; members invoked without an override throw an instructive failure. + /// + protected WidgetsClient() + { + } + + /// + /// Gets a bound 'WidgetClient'; the handle never caches server state. + /// + /// The 'widgetID' route value. + /// The bound 'WidgetClient'. + public virtual WidgetClient GetWidgetClient(string widgetId) + { + ArgumentException.ThrowIfNullOrEmpty(widgetId); + return new WidgetClient(Pipeline, widgetId); + } + + private Pipeline Pipeline => _pipeline ?? throw MockSeam.CreateError("WidgetsClient", "Pipeline"); + + /// + /// List the widget overview + /// + /// The per-call options. + /// The cancellation token. + /// The 'WidgetOverviewResponse' envelope. + /// The API returned an error status and NoThrow was not selected. + /// The server could not be reached or returned a malformed success body. + public virtual Task GetOverviewAsync(OpenCodeRequestOptions? options = null, CancellationToken cancellationToken = default) + { + return Pipeline.ExecuteAsync(HttpMethod.Get, OpenCodeRoutes.Widgets.GetOverview, WidgetOverviewResponseAdapter.Instance, options, cancellationToken); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/EnvelopeEmitterTests.Emit_Should_Produce_Guarded_Response_Envelopes.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/EnvelopeEmitterTests.Emit_Should_Produce_Guarded_Response_Envelopes.verified.txt new file mode 100644 index 000000000..c816a12c7 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/EnvelopeEmitterTests.Emit_Should_Produce_Guarded_Response_Envelopes.verified.txt @@ -0,0 +1,185 @@ +// File: PingResponse.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk; +/// +/// Represents the response of the 'GET /api/ping' operation. +/// +public sealed record PingResponse : OpenCodeResponse +{ + private readonly ExampleItem? _ping; + /// + /// Initializes a success instance of the 'PingResponse' envelope. + /// + public PingResponse() + { + } + + /// + /// Initializes an error-path instance; the payload stays unset behind its guard. + /// + [SetsRequiredMembers] + internal PingResponse(int status, OpenCodeError? error, string? rawBody) + { + Status = status; + IsError = true; + Error = error; + RawBody = rawBody; + Ping = null!; + } + + /// + /// Gets the Ping payload; guarded on the error path. + /// + public required ExampleItem Ping { get => _ping ?? throw new InvalidOperationException("The response is an error; check IsError before accessing Ping."); init => _ping = value; } + + /// + /// Prints the shared metadata and appends the payload only when it is present. + /// + protected override bool PrintMembers(StringBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + var printed = base.PrintMembers(builder); + if (_ping is null) + { + return printed; + } + + if (printed) + { + _ = builder.Append(", "); + } + + _ = builder.Append("Ping = ").Append(_ping); + return true; + } +} + +// File: WidgetItemResponse.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk; +/// +/// Represents the response of the 'GET /api/widget/{widgetID}/item/{itemID}' operation. +/// +public sealed record WidgetItemResponse : OpenCodeResponse +{ + private readonly ExampleItem? _item; + /// + /// Initializes a success instance of the 'WidgetItemResponse' envelope. + /// + public WidgetItemResponse() + { + } + + /// + /// Initializes an error-path instance; the payload stays unset behind its guard. + /// + [SetsRequiredMembers] + internal WidgetItemResponse(int status, OpenCodeError? error, string? rawBody) + { + Status = status; + IsError = true; + Error = error; + RawBody = rawBody; + Item = null!; + } + + /// + /// Gets the Item payload; guarded on the error path. + /// + public required ExampleItem Item { get => _item ?? throw new InvalidOperationException("The response is an error; check IsError before accessing Item."); init => _item = value; } + + /// + /// Prints the shared metadata and appends the payload only when it is present. + /// + protected override bool PrintMembers(StringBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + var printed = base.PrintMembers(builder); + if (_item is null) + { + return printed; + } + + if (printed) + { + _ = builder.Append(", "); + } + + _ = builder.Append("Item = ").Append(_item); + return true; + } +} + +// File: WidgetOverviewResponse.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk; +/// +/// Represents the response of the 'GET /api/widget-overview' operation. +/// +public sealed record WidgetOverviewResponse : OpenCodeResponse +{ + private readonly ExampleItem? _overview; + /// + /// Initializes a success instance of the 'WidgetOverviewResponse' envelope. + /// + public WidgetOverviewResponse() + { + } + + /// + /// Initializes an error-path instance; the payload stays unset behind its guard. + /// + [SetsRequiredMembers] + internal WidgetOverviewResponse(int status, OpenCodeError? error, string? rawBody) + { + Status = status; + IsError = true; + Error = error; + RawBody = rawBody; + Overview = null!; + } + + /// + /// Gets the Overview payload; guarded on the error path. + /// + public required ExampleItem Overview { get => _overview ?? throw new InvalidOperationException("The response is an error; check IsError before accessing Overview."); init => _overview = value; } + + /// + /// Prints the shared metadata and appends the payload only when it is present. + /// + protected override bool PrintMembers(StringBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + var printed = base.PrintMembers(builder); + if (_overview is null) + { + return printed; + } + + if (printed) + { + _ = builder.Append(", "); + } + + _ = builder.Append("Overview = ").Append(_overview); + return true; + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ModelEmitterTests.Emit_Should_Produce_Immutable_Annotated_Models.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ModelEmitterTests.Emit_Should_Produce_Immutable_Annotated_Models.verified.txt index 6a3f99131..49eeb69e1 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ModelEmitterTests.Emit_Should_Produce_Immutable_Annotated_Models.verified.txt +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ModelEmitterTests.Emit_Should_Produce_Immutable_Annotated_Models.verified.txt @@ -30,6 +30,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; namespace OpenCode.Sdk.Models; /// @@ -54,29 +55,13 @@ public sealed record ExampleItem /// Gets the item tags. /// [JsonPropertyName("tags")] - public IReadOnlyList Tags - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? Array.Empty() : new List(value).AsReadOnly(); - } - } = Array.Empty(); + public IReadOnlyList Tags { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).AsReadOnly()); } = Array.Empty(); /// /// Gets links by relation. /// [JsonPropertyName("links")] - public IReadOnlyDictionary Links - { - get; - init - { - // Source-generated deserialization passes null for an absent optional init-only collection. - field = value is null ? new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)) : new ReadOnlyDictionary(value.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal)); - } - } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); + public IReadOnlyDictionary Links { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary(input.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal))); } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); } // File: Models/ExampleMode.cs diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/OptionalCollectionInputEmitterTests.Emit_Should_Produce_The_Nullable_Input_Normalizer.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/OptionalCollectionInputEmitterTests.Emit_Should_Produce_The_Nullable_Input_Normalizer.verified.txt new file mode 100644 index 000000000..04724deb6 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/OptionalCollectionInputEmitterTests.Emit_Should_Produce_The_Nullable_Input_Normalizer.verified.txt @@ -0,0 +1,11 @@ +// File: Internal/Serialization/OptionalCollectionInput.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; + +namespace OpenCode.Sdk.Internal.Serialization; +internal static class OptionalCollectionInput +{ + internal static T Normalize(T? value, T empty, Func copy) + where T : class => value is null ? empty : copy(value); +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ResponseAdapterEmitterTests.Emit_Should_Produce_Status_Mapped_Adapters.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ResponseAdapterEmitterTests.Emit_Should_Produce_Status_Mapped_Adapters.verified.txt new file mode 100644 index 000000000..59583d658 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ResponseAdapterEmitterTests.Emit_Should_Produce_Status_Mapped_Adapters.verified.txt @@ -0,0 +1,122 @@ +// File: Internal/ResponseAdapters/PingResponseAdapter.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Internal.ResponseAdapters; +/// +/// Adapts the 'GET /api/ping' responses onto 'PingResponse'. +/// +internal sealed class PingResponseAdapter : ResponseAdapter +{ + private static readonly string[] Status400Tags = ["BadRequestError"]; + private PingResponseAdapter() + { + } + + /// + /// Gets the shared adapter instance. + /// + public static PingResponseAdapter Instance { get; } = new PingResponseAdapter(); + + /// + /// Maps one buffered response onto the typed envelope. + /// + public override PingResponse Adapt(int status, string rawBody) + { + ArgumentNullException.ThrowIfNull(rawBody); + return status switch + { + 200 => new PingResponse + { + Status = status, + Ping = ReadBarePayload(rawBody, OpenCodeJsonContext.Default.ExampleItem) + }, + >= 200 and < 300 => throw UndeclaredSuccessFailure(status), + 400 => new PingResponse(status, ReadTolerantError(rawBody, Status400Tags), rawBody), + _ => new PingResponse(status, ReadTolerantError(rawBody, null), rawBody)}; + } +} + +// File: Internal/ResponseAdapters/WidgetItemResponseAdapter.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Internal.ResponseAdapters; +/// +/// Adapts the 'GET /api/widget/{widgetID}/item/{itemID}' responses onto 'WidgetItemResponse'. +/// +internal sealed class WidgetItemResponseAdapter : ResponseAdapter +{ + private static readonly string[] Status400Tags = ["BadRequestError"]; + private static readonly string[] Status404Tags = ["GoneError", "MissingError"]; + private WidgetItemResponseAdapter() + { + } + + /// + /// Gets the shared adapter instance. + /// + public static WidgetItemResponseAdapter Instance { get; } = new WidgetItemResponseAdapter(); + + /// + /// Maps one buffered response onto the typed envelope. + /// + public override WidgetItemResponse Adapt(int status, string rawBody) + { + ArgumentNullException.ThrowIfNull(rawBody); + return status switch + { + 200 => new WidgetItemResponse + { + Status = status, + Item = ReadDataPayload(rawBody, OpenCodeJsonContext.Default.ExampleItem) + }, + >= 200 and < 300 => throw UndeclaredSuccessFailure(status), + 400 => new WidgetItemResponse(status, ReadTolerantError(rawBody, Status400Tags), rawBody), + 404 => new WidgetItemResponse(status, ReadTolerantError(rawBody, Status404Tags), rawBody), + _ => new WidgetItemResponse(status, ReadTolerantError(rawBody, null), rawBody)}; + } +} + +// File: Internal/ResponseAdapters/WidgetOverviewResponseAdapter.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Internal.ResponseAdapters; +/// +/// Adapts the 'GET /api/widget-overview' responses onto 'WidgetOverviewResponse'. +/// +internal sealed class WidgetOverviewResponseAdapter : ResponseAdapter +{ + private WidgetOverviewResponseAdapter() + { + } + + /// + /// Gets the shared adapter instance. + /// + public static WidgetOverviewResponseAdapter Instance { get; } = new WidgetOverviewResponseAdapter(); + + /// + /// Maps one buffered response onto the typed envelope. + /// + public override WidgetOverviewResponse Adapt(int status, string rawBody) + { + ArgumentNullException.ThrowIfNull(rawBody); + return status switch + { + 200 => new WidgetOverviewResponse + { + Status = status, + Overview = ReadBarePayload(rawBody, OpenCodeJsonContext.Default.ExampleItem) + }, + >= 200 and < 300 => throw UndeclaredSuccessFailure(status), + _ => new WidgetOverviewResponse(status, ReadTolerantError(rawBody, null), rawBody)}; + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/RoutesEmitterTests.Emit_Should_Produce_Escaped_Route_Builders.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/RoutesEmitterTests.Emit_Should_Produce_Escaped_Route_Builders.verified.txt new file mode 100644 index 000000000..ec1dd9f4c --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/RoutesEmitterTests.Emit_Should_Produce_Escaped_Route_Builders.verified.txt @@ -0,0 +1,60 @@ +// File: OpenCodeRoutes.cs +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System; + +namespace OpenCode.Sdk; +/// +/// Defines the wire route of every generated operation. +/// +public static class OpenCodeRoutes +{ + /// + /// Defines the 'Ping' routes. + /// + public static class Ping + { + /// + /// The 'GET /api/ping' route. + /// + public const string Get = "/api/ping"; + } + + /// + /// Defines the 'Widgets' routes. + /// + public static class Widgets + { + /// + /// The 'GET /api/widget/{widgetID}/item/{itemID}' route template. + /// + public const string GetItemTemplate = "/api/widget/{widgetID}/item/{itemID}"; + /// + /// Builds the '/api/widget/{widgetID}/item/{itemID}' route. + /// + /// The 'widgetID' route value. + /// The 'itemID' route value. + /// The escaped route. + public static string GetItem(string widgetId, string itemId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(widgetId); + if (widgetId is "." or "..") + { + throw new ArgumentException("Route values must not be dot segments.", nameof(widgetId)); + } + + ArgumentException.ThrowIfNullOrWhiteSpace(itemId); + if (itemId is "." or "..") + { + throw new ArgumentException("Route values must not be dot segments.", nameof(itemId)); + } + + return "/api/widget/" + Uri.EscapeDataString(widgetId) + "/item/" + Uri.EscapeDataString(itemId); + } + + /// + /// The 'GET /api/widget-overview' route. + /// + public const string GetOverview = "/api/widget-overview"; + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/UnionEmitterTests.Emit_Should_Produce_Tolerant_Marked_Union_Converters.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/UnionEmitterTests.Emit_Should_Produce_Tolerant_Marked_Union_Converters.verified.txt index 7977bccd5..41e320106 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/UnionEmitterTests.Emit_Should_Produce_Tolerant_Marked_Union_Converters.verified.txt +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/UnionEmitterTests.Emit_Should_Produce_Tolerant_Marked_Union_Converters.verified.txt @@ -46,6 +46,7 @@ public sealed record UnknownExampleEvent : ExampleEvent /// /// Gets the unrecognized 'type' marker. /// + [JsonPropertyName("type")] public override string Type => _marker; /// /// Gets the preserved raw JSON payload. @@ -87,8 +88,8 @@ internal sealed class ExampleEventJsonConverter : JsonConverter if (markerElement.ValueKind != JsonValueKind.String) throw new JsonException("The 'type' marker must be a string."); var marker = markerElement.GetString(); - if (marker is null) - throw new JsonException("The 'type' marker cannot be null."); + if (marker is null || string.IsNullOrWhiteSpace(marker)) + throw new JsonException("The 'type' marker must be a non-empty string."); if (TypesByTag.TryGetValue(marker, out var targetType)) { var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for ExampleEvent."); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingScenarioData.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingScenarioData.cs new file mode 100644 index 000000000..d800cbc2d --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingScenarioData.cs @@ -0,0 +1,45 @@ +using OpenCode.Sdk.Tools.Generator.Binding.Models; + +namespace OpenCode.Sdk.Tools.Tests.Support; + +internal static class BindingScenarioData +{ + public static OperationSelection Selection(params string[] operationIds) => + new() + { + OperationIds = operationIds, + }; + + public static GenerationCuration Curation( + IReadOnlyDictionary groups, + IReadOnlyDictionary? envelopePayloadNames = null, + IReadOnlyList? propertyOverrides = null) => + new() + { + Groups = groups, + EnvelopePayloadNames = envelopePayloadNames ?? new Dictionary(StringComparer.Ordinal), + PropertyOverrides = propertyOverrides ?? [], + }; + + public static Dictionary Groups(string wireName, GroupCuration group) => + new(StringComparer.Ordinal) + { + [wireName] = group, + }; + + public static GroupCuration RootGroup() => + new() + { + Placement = GroupPlacement.Root, + }; + + public static GroupCuration ClientGroup(string clientName = "Sessions", string? handleName = "SessionClient", + string? handleParameter = "sessionID") => + new() + { + Placement = GroupPlacement.Client, + ClientName = clientName, + HandleName = handleName, + HandleParameter = handleParameter, + }; +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs index 7e7090bc3..c5ee5ed43 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs @@ -12,11 +12,12 @@ internal sealed class BindingTestHost public BindingTestHost() { - var schemaNames = new SchemaNameResolver(); _binder = new SpecBinder( new ReachableSchemaCollector(), new CurationValidator(), - new SchemaPlanBinder(schemaNames)); + new SchemaNameResolver(), + new SchemaPlanBinder(), + new OperationPlanBinder()); } public EmitPlan Bind(SpecDocument document, OperationSelection selection, GenerationCuration curation) @@ -24,6 +25,14 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera return _binder.Bind(document, selection, curation); } + public static async Task IngestAsync(SpecScenario scenario, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(scenario); + + var context = scenario.Build(); + return await new SpecIngestion(context.FileSystem).IngestAsync(context.SpecPath, cancellationToken); + } + public async Task BindPinnedAsync(CancellationToken cancellationToken = default) { var fileSystem = new RealFileSystem(); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs index df2f3725a..8b63dd0e2 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs @@ -26,6 +26,7 @@ public static EmitPlan Create() SelectedOperationIds = ["v2.example.get"], Models = models, Unions = unions, + Clients = CreateClientPlans(), Registry = new RegistryPlan { TypeNames = @@ -45,6 +46,165 @@ public static EmitPlan Create() }; } + public static IReadOnlyList CreateClientPlans() => + [ + new ClientPlan + { + Name = "OpenCodeClient", + Namespace = "OpenCode.Sdk", + Role = ClientRole.Root, + SubClients = + [ + new ClientReferencePlan + { + PropertyName = "Widgets", + TypeName = "WidgetsClient", + }, + ], + Operations = [CreatePingOperation()], + }, + new ClientPlan + { + Name = "WidgetClient", + Namespace = "OpenCode.Sdk", + Role = ClientRole.Handle, + SubClients = [], + HandleParameter = CreateWidgetParameter(), + Operations = [CreateItemOperation()], + }, + new ClientPlan + { + Name = "WidgetsClient", + Namespace = "OpenCode.Sdk", + Role = ClientRole.Collection, + SubClients = [], + HandleFactory = new HandleFactoryPlan + { + MethodName = "GetWidgetClient", + HandleTypeName = "WidgetClient", + Parameter = CreateWidgetParameter(), + }, + Operations = [CreateOverviewOperation()], + }, + ]; + + private static OperationPlan CreatePingOperation() => + new() + { + MethodName = "GetPingAsync", + HttpMethod = "get", + RouteTemplate = "/api/ping", + RouteContainerName = "Ping", + RouteMemberName = "Get", + Parameters = [], + Envelope = new EnvelopePlan + { + ResponseTypeName = "PingResponse", + AdapterTypeName = "PingResponseAdapter", + PayloadName = "Ping", + PayloadTypeName = "ExampleItem", + HasDataEnvelope = false, + }, + ErrorMap = new ErrorMapPlan + { + Statuses = + [ + new ErrorStatusPlan + { + StatusCode = 400, + Tags = [new ErrorTagPlan { Tag = "BadRequestError", TypeName = "BadRequestError", }], + }, + ], + }, + Summary = "Check example ping", + Description = "Reports the example service state.", + }; + + private static OperationPlan CreateOverviewOperation() => + new() + { + MethodName = "GetOverviewAsync", + HttpMethod = "get", + RouteTemplate = "/api/widget-overview", + RouteContainerName = "Widgets", + RouteMemberName = "GetOverview", + Parameters = [], + Envelope = new EnvelopePlan + { + ResponseTypeName = "WidgetOverviewResponse", + AdapterTypeName = "WidgetOverviewResponseAdapter", + PayloadName = "Overview", + PayloadTypeName = "ExampleItem", + HasDataEnvelope = false, + }, + ErrorMap = new ErrorMapPlan + { + Statuses = [], + }, + Summary = "List the widget overview", + Description = null, + }; + + private static OperationPlan CreateItemOperation() => + new() + { + MethodName = "GetItemAsync", + HttpMethod = "get", + RouteTemplate = "/api/widget/{widgetID}/item/{itemID}", + RouteContainerName = "Widgets", + RouteMemberName = "GetItem", + Parameters = + [ + CreateWidgetParameter(), + new OperationParameterPlan + { + WireName = "itemID", + Name = "itemId", + TypeName = "string", + IsHandleParameter = false, + }, + ], + Envelope = new EnvelopePlan + { + ResponseTypeName = "WidgetItemResponse", + AdapterTypeName = "WidgetItemResponseAdapter", + PayloadName = "Item", + PayloadTypeName = "ExampleItem", + HasDataEnvelope = true, + }, + ErrorMap = new ErrorMapPlan + { + Statuses = + [ + new ErrorStatusPlan + { + StatusCode = 400, + Tags = [new ErrorTagPlan { Tag = "BadRequestError", TypeName = "BadRequestError", }], + }, + new ErrorStatusPlan + { + StatusCode = 404, + Tags = + [ + new ErrorTagPlan { Tag = "GoneError", TypeName = "GoneError", }, + new ErrorTagPlan { Tag = "MissingError", TypeName = "MissingError", }, + ], + }, + ], + }, + Summary = "Get one widget item", + Description = "Retrieves one item owned by the widget.", + }; + + private static OperationParameterPlan CreateWidgetParameter() => + new() + { + WireName = "widgetID", + Name = "widgetId", + TypeName = "string", + IsHandleParameter = true, + }; + public static EmitPlan CreateModelSnapshot() { var plan = Create(); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/GeneratedSourceCompiler.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/GeneratedSourceCompiler.cs index 281103a67..68ae73044 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/GeneratedSourceCompiler.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/GeneratedSourceCompiler.cs @@ -14,14 +14,60 @@ internal static class GeneratedSourceCompiler private static readonly PortableExecutableReference[] References = CreateReferences(); private static readonly AnalyzerFileReference SourceGenerator = CreateSourceGeneratorReference(); - public static Diagnostic[] Compile(IReadOnlyList sources) + /// + /// Compiles the generated sources together with the hand-written SDK sources so generated + /// clients resolve the behavior core; an emitted path shadows its committed twin. + /// + /// The freshly emitted sources. + /// Warning-or-worse diagnostics. + public static async Task CompileWithSdkCoreAsync(IReadOnlyList sources) { ArgumentNullException.ThrowIfNull(sources); + + // The SDK project compiles with implicit usings; the probe replays the same set. + const string implicitUsings = """ + global using System; + global using System.Collections.Generic; + global using System.IO; + global using System.Linq; + global using System.Net.Http; + global using System.Threading; + global using System.Threading.Tasks; + """; + var fileSystem = new RealFileSystem(); + var root = fileSystem.Path.Combine(AppContext.BaseDirectory, "Fixtures", "SdkSource"); + var coreTrees = new List + { + CSharpSyntaxTree.ParseText(implicitUsings, ParseOptions, "sdk:ImplicitUsings.cs", Encoding.UTF8), + }; + foreach (var entry in fileSystem.Directory + .EnumerateFiles(root, "*.cs", SearchOption.AllDirectories) + .Select(path => (Path: path, Relative: fileSystem.Path.GetRelativePath(root, path).Replace('\\', '/'))) + .OrderBy(static entry => entry.Relative, StringComparer.Ordinal)) + { + var text = await fileSystem.File.ReadAllTextAsync(entry.Path, CancellationToken.None); + + // The committed generated layer has its own radar (generate --verify); the probe + // pairs the freshly emitted layer with the hand-written core only. + if (text.StartsWith("// Generated by OpenCode.Sdk.Tools", StringComparison.Ordinal)) + { + continue; + } + + coreTrees.Add(CSharpSyntaxTree.ParseText(text, ParseOptions, $"sdk:{entry.Relative}", Encoding.UTF8)); + } + + return Compile(sources, coreTrees); + } + + private static Diagnostic[] Compile(IReadOnlyList sources, IReadOnlyList extraTrees) + { var syntaxTrees = sources.Select(static source => CSharpSyntaxTree.ParseText( - Encoding.UTF8.GetString(source.Utf8Source.Span), - ParseOptions, - source.RelativePath, - Encoding.UTF8)); + Encoding.UTF8.GetString(source.Utf8Source.Span), + ParseOptions, + source.RelativePath, + Encoding.UTF8)) + .Concat(extraTrees); var compilation = CSharpCompilation.Create( "GeneratedSourceProbe", syntaxTrees, diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs index cac1866ff..a603d7ba2 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs @@ -12,6 +12,9 @@ internal static class GenerationTestData public const string OutputRoot = "src/OpenCode.Sdk"; public const string ProjectPath = "src/OpenCode.Sdk/OpenCode.Sdk.csproj"; + /// Provenance-headed content for a generated file that already sits on disk. + public static string OwnedContent(string content) => $"{GenerationProvenance.Header}{content}"; + private const string Curation = """ { "groups": { @@ -34,8 +37,11 @@ public static MockFileSystem CreateFileSystem() public static MockFileSystem CreateCommandFileSystem() { var spec = new SpecDocumentBuilder() - .WithOperation("v2.health.get", path: "/api/health") - .WithOperation("session.list", path: "/session") + .WithSchema("ExampleHealth", schema => schema.Type("object") + .Property("healthy", property => property.Type("boolean"), required: true)) + .WithOperation("v2.health.get", path: "/api/health", configure: operation => operation + .Response(200, "application/json", schema => schema.Ref("ExampleHealth"))) + .WithOperation("v2.session.list", path: "/api/session") .BuildJson(); var fileSystem = CreateFileSystem(); fileSystem.Initialize().With( @@ -49,6 +55,6 @@ public static GeneratedSource Source(string relativePath, string source) => new() { RelativePath = relativePath, - Utf8Source = Encoding.UTF8.GetBytes(source), + Utf8Source = Encoding.UTF8.GetBytes(OwnedContent(source)), }; } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/OperationBuilder.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/OperationBuilder.cs index 9b7d5e918..da95191c4 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/OperationBuilder.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/OperationBuilder.cs @@ -100,6 +100,12 @@ public OperationBuilder Response(int status, string? mediaType = null, Action? configureS _responses[FormatStatus(status)] = response; } + public void Remove(int status) + { + ValidateStatus(status); + + _ = _responses.Remove(FormatStatus(status)); + } + public void AddFromFixture(int status, string mediaType, string fixtureName) { ValidateStatus(status); diff --git a/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs b/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs index 02919293c..fe220969d 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs @@ -107,8 +107,7 @@ public async Task RunAsync_Should_Generate_Selected_Output() await Assert.That(result.ExitCode).IsEqualTo(0); await Assert.That(fileSystem.File.Exists(GenerationTestData.ManifestPath)).IsTrue(); await Assert.That(fileSystem.File.Exists(GenerationTestData.MarkerPath)).IsTrue(); - await Assert.That(result.Output).Contains("Pending modern"); - await Assert.That(result.Output).Contains("Pending legacy"); + await Assert.That(result.Output).Contains("Pending operations"); } [Test] @@ -146,7 +145,7 @@ public async Task RunAsync_Should_Return_Nonzero_And_Report_Verify_Drift() var generateTester = new CommandAppTester(registrar); generateTester.Configure(ToolApp.Configure); _ = await generateTester.RunAsync(["generate"]); - await fileSystem.File.WriteAllTextAsync(generatedPath, "drift", CancellationToken.None); + await fileSystem.File.WriteAllTextAsync(generatedPath, GenerationTestData.OwnedContent("drift"), CancellationToken.None); var verifyTester = new CommandAppTester(registrar); verifyTester.Configure(ToolApp.Configure); diff --git a/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs b/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs index 11a655340..86f4af107 100644 --- a/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs +++ b/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs @@ -25,17 +25,16 @@ public GenerateCommand(IAnsiConsole console, GenerationCoordinator coordinator, _logger = logger; } - protected override async Task ExecuteAsync( - CommandContext context, - Settings settings, - CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(settings); + LogInvocation(_logger); - var report = await _coordinator.GenerateAsync( - GenerationRequest.RepositoryDefault(settings.Verify), - cancellationToken).ConfigureAwait(false); + + var generationRequest = GenerationRequest.RepositoryDefault(settings.Verify); + var report = await _coordinator.GenerateAsync(generationRequest, cancellationToken).ConfigureAwait(false); + WriteSummary(report); if (settings.Verify && report.WriteResult.HasChanges) @@ -56,32 +55,25 @@ protected override async Task ExecuteAsync( private void WriteSummary(GenerationReport report) { - _console.MarkupLine(string.Concat( - "[grey]Selected operations:[/] ", - report.SelectedOperationIds.Count.ToString(CultureInfo.InvariantCulture))); - _console.MarkupLine(string.Concat( - "[yellow]Pending modern operations:[/] ", - report.PendingModernOperationIds.Count.ToString(CultureInfo.InvariantCulture))); - _console.MarkupLine(string.Concat( - "[yellow]Pending legacy operations:[/] ", - report.PendingLegacyOperationIds.Count.ToString(CultureInfo.InvariantCulture))); + _console.MarkupLine($"[grey]Selected operations:[/] {report.SelectedOperationIds.Count.ToString(CultureInfo.InvariantCulture)}"); + _console.MarkupLine($"[yellow]Pending operations:[/] {report.PendingOperationIds.Count.ToString(CultureInfo.InvariantCulture)}"); } private void WritePaths(WriteResult result) { foreach (var path in result.CreatedPaths) { - _console.MarkupLine(string.Concat("[green]+[/] ", Markup.Escape(path))); + _console.MarkupLine($"[green]+[/] {Markup.Escape(path)}"); } foreach (var path in result.ChangedPaths) { - _console.MarkupLine(string.Concat("[yellow]~[/] ", Markup.Escape(path))); + _console.MarkupLine($"[yellow]~[/] {Markup.Escape(path)}"); } foreach (var path in result.DeletedPaths) { - _console.MarkupLine(string.Concat("[red]-[/] ", Markup.Escape(path))); + _console.MarkupLine($"[red]-[/] {Markup.Escape(path)}"); } } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/BindingErrorCollector.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/BindingErrorCollector.cs index 28cfd31cd..5b18ae137 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/BindingErrorCollector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/BindingErrorCollector.cs @@ -12,6 +12,7 @@ public void Add(BindingErrorCategory category, string subject, string problem) { ArgumentException.ThrowIfNullOrWhiteSpace(subject); ArgumentException.ThrowIfNullOrWhiteSpace(problem); + _errors.Add(new BindingError(category, subject, problem)); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs index 76dfd77be..f5943077f 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs @@ -88,12 +88,6 @@ public static IReadOnlyList SplitWords(string value) private static void AppendPascalWord(StringBuilder result, string word) { - if (word.Length is 2 && word.All(char.IsLetter)) - { - _ = result.Append(word.ToUpperInvariant()); - return; - } - _ = result.Append(char.ToUpperInvariant(word[0])); if (word.Length > 1) { diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs index 3ae1092cc..02cde23e1 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs @@ -21,7 +21,7 @@ public void Validate(SpecDocument document, IReadOnlyList selecte var documentIds = document.Operations.Select(static operation => operation.OperationId).ToHashSet(_comparer); var documentGroups = document.Operations.Select(GetGroup).ToHashSet(_comparer); ValidateGroups(selected, selectedGroups, documentGroups, curation, errors); - ValidateEnvelopeNames(selected, selectedIds, documentIds, curation, errors); + ValidateEnvelopeNames(selectedIds, documentIds, curation, errors); ValidatePropertyOverrides(document, reachable, curation, errors); } @@ -48,13 +48,12 @@ private static void ValidateGroups(IReadOnlyList selected, HashSe } } - private static void ValidateGroupShape(string wireName, GroupCuration group, IReadOnlyList selected, - BindingErrorCollector errors) + private static void ValidateGroupShape(string wireName, GroupCuration group, IReadOnlyList selected, BindingErrorCollector errors) { switch (group.Placement) { - case GroupPlacement.Root when group.ClientName is not null || group.HandleName is not null: - errors.Add(BindingErrorCategory.Curation, wireName, "root group cannot declare clientName or handleName"); + case GroupPlacement.Root when group.ClientName is not null || group.HandleName is not null || group.HandleParameter is not null: + errors.Add(BindingErrorCategory.Curation, wireName, "root group cannot declare clientName, handleName, or handleParameter"); break; case GroupPlacement.Client when string.IsNullOrWhiteSpace(group.ClientName): errors.Add(BindingErrorCategory.Curation, wireName, "client group must declare clientName"); @@ -71,32 +70,69 @@ private static void ValidateGroupShape(string wireName, GroupCuration group, IRe errors.Add(BindingErrorCategory.Naming, wireName, $"handle name '{group.HandleName}' is not a valid C# identifier"); } - var requiresSessionHandle = selected.Any(operation => string.Equals(GetGroup(operation), wireName, StringComparison.Ordinal) - && operation.Parameters.Any(static parameter => parameter is - { - Name: "sessionID", - Location: SpecParameterLocation.Path, - })); - if (requiresSessionHandle && string.IsNullOrWhiteSpace(group.HandleName)) + if ((group.HandleName is not null) != (group.HandleParameter is not null)) + { + errors.Add(BindingErrorCategory.Curation, wireName, "handleName and handleParameter must be declared together"); + return; + } + + if (group.HandleParameter is null) + { + return; + } + + if (string.IsNullOrWhiteSpace(group.HandleParameter)) { - errors.Add(BindingErrorCategory.Curation, wireName, "session-scoped selected operation requires handleName"); + errors.Add(BindingErrorCategory.Curation, wireName, "handleParameter cannot be blank"); + return; } + + ValidateHandleParameterCoverage(wireName, group, selected, errors); } - private static void ValidateEnvelopeNames(IReadOnlyList selected, HashSet selectedIds, - HashSet documentIds, GenerationCuration curation, BindingErrorCollector errors) + /// + /// Coverage is selection-scoped: during staged generation only selected operations can + /// witness the handle parameter, and a group with no selected operations already fails + /// the global orphan check. + /// + private static void ValidateHandleParameterCoverage(string wireName, GroupCuration group, IReadOnlyList selected, + BindingErrorCollector errors) { - foreach (var operation in selected) + if (group.Placement is not GroupPlacement.Client) { - var requiresPayloadName = operation.Responses.Any(static response => response.StatusCode is >= 200 and < 300 - && response.EnvelopeShape is SpecEnvelopeShape.Data or SpecEnvelopeShape.DataLocation - or SpecEnvelopeShape.CursorData or SpecEnvelopeShape.DataHasMore); - if (requiresPayloadName && !curation.EnvelopePayloadNames.ContainsKey(operation.OperationId)) - { - errors.Add(BindingErrorCategory.Curation, operation.OperationId, "selected response envelope has no payload name"); - } + return; } + var groupOperations = selected + .Where(operation => string.Equals(GetGroup(operation), wireName, StringComparison.Ordinal)) + .ToArray(); + if (groupOperations.Length is 0) + { + return; + } + + var covered = groupOperations.Any(operation => operation.Parameters.Any(parameter => parameter is + { + Location: SpecParameterLocation.Path, + IsRequired: true, + } && string.Equals(parameter.Name, group.HandleParameter, StringComparison.Ordinal))); + if (!covered) + { + errors.Add( + BindingErrorCategory.Curation, + wireName, + $"handle parameter '{group.HandleParameter}' does not name a required path parameter on any selected operation in the group"); + } + } + + /// + /// Payload names derive mechanically from the operation subject; curated entries are + /// overrides only (maintainer, 2026-08-13), so validation covers orphans and identifier + /// legality, never presence. + /// + private static void ValidateEnvelopeNames(HashSet selectedIds, HashSet documentIds, + GenerationCuration curation, BindingErrorCollector errors) + { foreach (var (operationId, payloadName) in curation.EnvelopePayloadNames.OrderBy(static pair => pair.Key, StringComparer.Ordinal)) { if (!documentIds.Contains(operationId)) @@ -115,8 +151,7 @@ private static void ValidateEnvelopeNames(IReadOnlyList selected, } } - private static void ValidatePropertyOverrides(SpecDocument document, ReachableSchemaSet reachable, - GenerationCuration curation, BindingErrorCollector errors) + private static void ValidatePropertyOverrides(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation, BindingErrorCollector errors) { var reachableKeys = reachable.GraphKeys.ToHashSet(StringComparer.Ordinal); var targets = new HashSet<(string Schema, string Property)>(); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/BindingErrorCategory.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/BindingErrorCategory.cs index d231e729b..18f7533c8 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/BindingErrorCategory.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/BindingErrorCategory.cs @@ -14,4 +14,7 @@ public enum BindingErrorCategory /// A selected wire name cannot be mapped to a unique C# identifier. Naming = 3, + + /// A selected operation's wire contract cannot be bound by the M1 operation binder. + Operation = 4, } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientPlan.cs new file mode 100644 index 000000000..b69f32368 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientPlan.cs @@ -0,0 +1,37 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record ClientPlan +{ + public required string Name { get; init; } + + public required string Namespace { get; init; } + + public required ClientRole Role { get; init; } + + /// Gets the group client properties exposed by the root client. + public required IReadOnlyList SubClients + { + get; + init + { + ArgumentNullException.ThrowIfNull(value); + field = Array.AsReadOnly([.. value]); + } + } = Array.AsReadOnly(Array.Empty()); + + /// Gets the handle factory a collection client exposes, when its group declares a handle. + public HandleFactoryPlan? HandleFactory { get; init; } + + /// Gets the partially applied parameter a handle client carries as immutable state. + public OperationParameterPlan? HandleParameter { get; init; } + + public required IReadOnlyList Operations + { + get; + init + { + ArgumentNullException.ThrowIfNull(value); + field = Array.AsReadOnly([.. value]); + } + } = Array.AsReadOnly(Array.Empty()); +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientReferencePlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientReferencePlan.cs new file mode 100644 index 000000000..05711244c --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientReferencePlan.cs @@ -0,0 +1,8 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record ClientReferencePlan +{ + public required string PropertyName { get; init; } + + public required string TypeName { get; init; } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientRole.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientRole.cs new file mode 100644 index 000000000..6acffa45f --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientRole.cs @@ -0,0 +1,8 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal enum ClientRole +{ + Root = 0, + Collection = 1, + Handle = 2, +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/EmitPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/EmitPlan.cs index aec3e283a..d639a3515 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/EmitPlan.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/EmitPlan.cs @@ -34,6 +34,16 @@ public required IReadOnlyList Unions public required RegistryPlan Registry { get; init; } + public required IReadOnlyList Clients + { + get; + init + { + ArgumentNullException.ThrowIfNull(value); + field = Array.AsReadOnly([.. value]); + } + } = Array.AsReadOnly(Array.Empty()); + public required IReadOnlyList PendingOperations { get; diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/EnvelopePlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/EnvelopePlan.cs new file mode 100644 index 000000000..e17e50256 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/EnvelopePlan.cs @@ -0,0 +1,15 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record EnvelopePlan +{ + public required string ResponseTypeName { get; init; } + + public required string AdapterTypeName { get; init; } + + public required string PayloadName { get; init; } + + public required string PayloadTypeName { get; init; } + + /// Gets a value indicating whether the wire success body wraps the payload in a data property. + public required bool HasDataEnvelope { get; init; } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorMapPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorMapPlan.cs new file mode 100644 index 000000000..2ef916e4d --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorMapPlan.cs @@ -0,0 +1,14 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record ErrorMapPlan +{ + public required IReadOnlyList Statuses + { + get; + init + { + ArgumentNullException.ThrowIfNull(value); + field = Array.AsReadOnly([.. value]); + } + } = Array.AsReadOnly(Array.Empty()); +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorStatusPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorStatusPlan.cs new file mode 100644 index 000000000..2ae455979 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorStatusPlan.cs @@ -0,0 +1,16 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record ErrorStatusPlan +{ + public required int StatusCode { get; init; } + + public required IReadOnlyList Tags + { + get; + init + { + ArgumentNullException.ThrowIfNull(value); + field = Array.AsReadOnly([.. value]); + } + } = Array.AsReadOnly(Array.Empty()); +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorTagPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorTagPlan.cs new file mode 100644 index 000000000..a78d1d97e --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorTagPlan.cs @@ -0,0 +1,8 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record ErrorTagPlan +{ + public required string Tag { get; init; } + + public required string TypeName { get; init; } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/GroupCuration.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/GroupCuration.cs index 0270c47be..65802e800 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/GroupCuration.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/GroupCuration.cs @@ -12,4 +12,7 @@ internal sealed record GroupCuration [JsonPropertyName("handleName")] public string? HandleName { get; init; } + + [JsonPropertyName("handleParameter")] + public string? HandleParameter { get; init; } } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/HandleFactoryPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/HandleFactoryPlan.cs new file mode 100644 index 000000000..fab084967 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/HandleFactoryPlan.cs @@ -0,0 +1,10 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record HandleFactoryPlan +{ + public required string MethodName { get; init; } + + public required string HandleTypeName { get; init; } + + public required OperationParameterPlan Parameter { get; init; } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationParameterPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationParameterPlan.cs new file mode 100644 index 000000000..bd9527f7a --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationParameterPlan.cs @@ -0,0 +1,13 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record OperationParameterPlan +{ + public required string WireName { get; init; } + + public required string Name { get; init; } + + public required string TypeName { get; init; } + + /// Gets a value indicating whether the bound handle supplies this parameter. + public required bool IsHandleParameter { get; init; } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationPlan.cs new file mode 100644 index 000000000..9c0acc388 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationPlan.cs @@ -0,0 +1,36 @@ +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record OperationPlan +{ + public required string MethodName { get; init; } + + /// Gets the invariant-lowercase HTTP method. + public required string HttpMethod { get; init; } + + public required string RouteTemplate { get; init; } + + /// Gets the nested container the route members live in (client name, or the group for root placement). + public required string RouteContainerName { get; init; } + + /// Gets the route member name inside its container; containers never restate the group in members. + public required string RouteMemberName { get; init; } + + /// Gets every route parameter in template order, including the handle-supplied one. + public required IReadOnlyList Parameters + { + get; + init + { + ArgumentNullException.ThrowIfNull(value); + field = Array.AsReadOnly([.. value]); + } + } = Array.AsReadOnly(Array.Empty()); + + public required EnvelopePlan Envelope { get; init; } + + public required ErrorMapPlan ErrorMap { get; init; } + + public string? Summary { get; init; } + + public string? Description { get; init; } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PendingOperationPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PendingOperationPlan.cs index 762a30aa3..51e86f7db 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PendingOperationPlan.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PendingOperationPlan.cs @@ -1,10 +1,6 @@ -using OpenCode.Sdk.Tools.Generator.Ingestion.Models; - namespace OpenCode.Sdk.Tools.Generator.Binding.Models; internal sealed record PendingOperationPlan { public required string OperationId { get; init; } - - public required SpecSurface Surface { get; init; } } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionFixedMarkerPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionFixedMarkerPlan.cs new file mode 100644 index 000000000..eb01456a0 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionFixedMarkerPlan.cs @@ -0,0 +1,14 @@ +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record UnionFixedMarkerPlan +{ + public required string WireName { get; init; } + + public required string Name { get; init; } + + public required LiteralKind Kind { get; init; } + + public required string Value { get; init; } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionPlan.cs index 97ff2ac8d..5381892cf 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionPlan.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionPlan.cs @@ -27,4 +27,10 @@ public required IReadOnlyList Variants } = Array.AsReadOnly(Array.Empty()); public string? Description { get; init; } + + /// Gets the outer union base type when this union is itself a nested variant. + public string? BaseTypeName { get; init; } + + /// Gets the outer marker this nested union fixes to one value for all its variants. + public UnionFixedMarkerPlan? FixedMarker { get; init; } } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionVariantPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionVariantPlan.cs index e2ae45e51..328e2785a 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionVariantPlan.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionVariantPlan.cs @@ -5,4 +5,7 @@ internal sealed record UnionVariantPlan public required string TypeName { get; init; } public required string Tag { get; init; } + + /// Gets whether the variant is itself a nested marked union base. + public bool IsNestedUnion { get; init; } } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs new file mode 100644 index 000000000..8614d3425 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs @@ -0,0 +1,50 @@ +using OpenCode.Sdk.Tools.Generator.Binding.Models; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding; + +/// Derives C# member and type names for one bound operation from its identifier segments. +internal static class OperationNamePolicy +{ + public static string MethodName(SpecOperation operation, GroupPlacement placement) + { + ArgumentNullException.ThrowIfNull(operation); + + var subject = Subject(operation); + if (subject.Length is 0 && placement is GroupPlacement.Root) + { + subject = CSharpNamePolicy.ToPascalCase(operation.Segments[0]); + } + + return $"{CSharpNamePolicy.ToPascalCase(operation.Method)}{subject}Async"; + } + + public static string ResponseTypeName(SpecOperation operation) + { + ArgumentNullException.ThrowIfNull(operation); + + return $"{CSharpNamePolicy.ToPascalCase(operation.Segments[0])}{Subject(operation)}Response"; + } + + public static string RouteMemberName(SpecOperation operation) + { + ArgumentNullException.ThrowIfNull(operation); + + return $"{CSharpNamePolicy.ToPascalCase(operation.Method)}{Subject(operation)}"; + } + + public static string PayloadName(SpecOperation operation) + { + ArgumentNullException.ThrowIfNull(operation); + + var subject = Subject(operation); + return subject.Length is 0 ? CSharpNamePolicy.ToPascalCase(operation.Segments[0]) : subject; + } + + /// The operation's subject: every segment after the group that does not restate the HTTP method. + private static string Subject(SpecOperation operation) => + string.Concat(operation.Segments + .Skip(1) + .Where(segment => !string.Equals(segment, operation.Method, StringComparison.Ordinal)) + .Select(CSharpNamePolicy.ToPascalCase)); +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs new file mode 100644 index 000000000..3a946b982 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs @@ -0,0 +1,554 @@ +using System.Diagnostics; +using System.Globalization; +using OpenCode.Sdk.Tools.Generator.Binding.Models; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding; + +internal sealed class OperationPlanBinder +{ + private const string ClientNamespace = "OpenCode.Sdk"; + private const string RootClientName = "OpenCodeClient"; + + /// + /// Member names every generated response envelope inherits from the response spine; a + /// payload landing on one of them needs a curated override. + /// + private static readonly string[] ReservedPayloadNames = ["Error", "IsError", "RawBody", "Status"]; + + private readonly StringComparer _comparer = StringComparer.Ordinal; + + public IReadOnlyList Bind(SpecDocument document, IReadOnlyList selected, + GenerationCuration curation, IReadOnlyDictionary typeNames, BindingErrorCollector errors) + { + ArgumentNullException.ThrowIfNull(document); + ArgumentNullException.ThrowIfNull(selected); + ArgumentNullException.ThrowIfNull(curation); + ArgumentNullException.ThrowIfNull(typeNames); + ArgumentNullException.ThrowIfNull(errors); + + var bound = new List(selected.Count); + foreach (var operation in selected.OrderBy(static operation => operation.OperationId, _comparer)) + { + var operationPlan = new SingleOperationBinder(document, operation, curation, typeNames, errors).Bind(); + if (operationPlan is not null) + { + bound.Add(operationPlan); + } + } + + if (bound.Count is 0) + { + return []; + } + + var clients = AssembleClients(bound); + CheckMemberCollisions(clients, errors); + CheckTypeNameCollisions(clients, bound, errors); + return clients; + } + + private List AssembleClients(List bound) + { + var rootOperations = bound + .Where(static operation => operation.Row.Placement is GroupPlacement.Root) + .Select(static operation => operation.Plan) + .OrderBy(static plan => plan.MethodName, _comparer) + .ToArray(); + var subClients = new List(); + var clients = new List(); + foreach (var group in bound + .Where(static operation => operation.Row.Placement is GroupPlacement.Client) + .GroupBy(static operation => operation.Group, _comparer) + .OrderBy(static group => group.Key, _comparer)) + { + var members = group.ToArray(); + var row = members[0].Row; + var collectionName = $"{row.ClientName}Client"; + subClients.Add(new ClientReferencePlan + { + PropertyName = row.ClientName!, + TypeName = collectionName, + }); + clients.AddRange(CreateGroupClients(members, row, collectionName)); + } + + clients.Add(new ClientPlan + { + Name = RootClientName, + Namespace = ClientNamespace, + Role = ClientRole.Root, + SubClients = [.. subClients.OrderBy(static reference => reference.PropertyName, _comparer)], + Operations = rootOperations, + }); + + return + [ + .. clients.OrderBy(static client => client.Role is ClientRole.Root ? 0 : 1) + .ThenBy(static client => client.Name, StringComparer.Ordinal), + ]; + } + + private IEnumerable CreateGroupClients(IReadOnlyList group, GroupCuration row, string collectionName) + { + var handleOperations = group.Where(static operation => operation.IsHandleOperation).ToArray(); + var collectionOperations = group + .Where(static operation => !operation.IsHandleOperation) + .Select(static operation => operation.Plan) + .OrderBy(static plan => plan.MethodName, _comparer) + .ToArray(); + var handleParameter = handleOperations + .OrderBy(static operation => operation.Plan.MethodName, _comparer) + .SelectMany(static operation => operation.Plan.Parameters) + .FirstOrDefault(static parameter => parameter.IsHandleParameter); + + yield return new ClientPlan + { + Name = collectionName, + Namespace = ClientNamespace, + Role = ClientRole.Collection, + SubClients = [], + HandleFactory = handleParameter is null ? null : new HandleFactoryPlan + { + MethodName = $"Get{row.HandleName}", + HandleTypeName = row.HandleName!, + Parameter = handleParameter, + }, + Operations = collectionOperations, + }; + + if (handleParameter is not null) + { + yield return new ClientPlan + { + Name = row.HandleName!, + Namespace = ClientNamespace, + Role = ClientRole.Handle, + SubClients = [], + HandleParameter = handleParameter, + Operations = + [ + .. handleOperations + .Select(static operation => operation.Plan) + .OrderBy(static plan => plan.MethodName, _comparer), + ], + }; + } + } + + private void CheckMemberCollisions(List clients, BindingErrorCollector errors) + { + foreach (var client in clients) + { + var members = client.Operations.Select(static operation => operation.MethodName) + .Concat(client.SubClients.Select(static reference => reference.PropertyName)); + if (client.HandleFactory is not null) + { + members = members.Append(client.HandleFactory.MethodName); + } + + foreach (var member in members.GroupBy(static member => member, _comparer) + .Where(static member => member.Skip(1).Any()) + .OrderBy(static member => member.Key, _comparer)) + { + errors.Add(BindingErrorCategory.Naming, client.Name, $"multiple members map to C# name '{member.Key}'"); + } + } + } + + private void CheckTypeNameCollisions(List clients, List bound, BindingErrorCollector errors) + { + var owners = new HashSet(_comparer); + foreach (var name in clients.Select(static client => client.Name).Where(name => !owners.Add(name))) + { + errors.Add(BindingErrorCategory.Naming, name, $"client type name '{name}' collides with another generated type"); + } + + foreach (var operation in bound.Where(operation => !owners.Add(operation.Plan.Envelope.ResponseTypeName))) + { + errors.Add( + BindingErrorCategory.Naming, + operation.OperationId, + $"multiple operations map to response type '{operation.Plan.Envelope.ResponseTypeName}'"); + } + + var routeMembers = new HashSet(_comparer); + foreach (var operation in bound.Where(operation => + !routeMembers.Add($"{operation.Plan.RouteContainerName}.{operation.Plan.RouteMemberName}"))) + { + errors.Add( + BindingErrorCategory.Naming, + operation.OperationId, + $"multiple operations map to route member '{operation.Plan.RouteContainerName}.{operation.Plan.RouteMemberName}'"); + } + } + + private sealed record BoundOperation + { + public required string OperationId { get; init; } + + public required string Group { get; init; } + + public required GroupCuration Row { get; init; } + + public required bool IsHandleOperation { get; init; } + + public required OperationPlan Plan { get; init; } + } + + /// Binds one selected operation, batching every wire-contract refusal it finds. + private sealed class SingleOperationBinder(SpecDocument document, SpecOperation operation, + GenerationCuration curation, IReadOnlyDictionary typeNames, BindingErrorCollector errors) + { + private readonly GenerationCuration _curation = curation; + private readonly SpecDocument _document = document; + private readonly BindingErrorCollector _errors = errors; + private readonly SpecOperation _operation = operation; + private readonly IReadOnlyDictionary _typeNames = typeNames; + + public BoundOperation? Bind() + { + var group = _operation.Segments[0]; + if (!_curation.Groups.TryGetValue(group, out var row)) + { + // The curation validator already reported the missing row. + return null; + } + + if (!CheckWireShape()) + { + return null; + } + + var success = _operation.Responses.Single(static response => response.StatusCode is 200); + var envelope = BindEnvelope(success); + var errorMap = BindErrorMap(); + var parameters = BindParameters(row); + if (envelope is null || errorMap is null || parameters is null) + { + return null; + } + + return new BoundOperation + { + OperationId = _operation.OperationId, + Group = group, + Row = row, + IsHandleOperation = parameters.Any(static parameter => parameter.IsHandleParameter), + Plan = new OperationPlan + { + MethodName = OperationNamePolicy.MethodName(_operation, row.Placement), + HttpMethod = _operation.Method, + RouteTemplate = _operation.Path, + RouteContainerName = row.ClientName ?? CSharpNamePolicy.ToPascalCase(group), + RouteMemberName = OperationNamePolicy.RouteMemberName(_operation), + Parameters = parameters, + Envelope = envelope, + ErrorMap = errorMap, + Summary = _operation.Summary, + Description = _operation.Description, + }, + }; + } + + private bool CheckWireShape() + { + var before = _errors.Count; + if (!string.Equals(_operation.Method, "get", StringComparison.Ordinal)) + { + Refuse("only GET operations are supported in M1"); + } + + if (_operation.HasWildcardPath) + { + Refuse("wildcard paths are not supported in M1"); + } + + if (_operation.IsWebSocket) + { + Refuse("WebSocket operations are not supported in M1"); + } + + if (_operation.IsSse) + { + Refuse("event-stream responses are not supported in M1"); + } + + if (_operation.RequestBody is not null) + { + Refuse("request bodies are not supported in M1"); + } + + CheckParameterShapes(); + CheckStatusShape(); + return _errors.Count == before; + } + + private void CheckParameterShapes() + { + foreach (var parameter in _operation.Parameters) + { + if (parameter is not { Location: SpecParameterLocation.Path, IsRequired: true, IsDeepObject: false }) + { + Refuse($"parameter '{parameter.Name}' must be a required path parameter"); + continue; + } + + if (Resolve(parameter.Schema) is not PrimitiveNode { Kind: PrimitiveKind.String }) + { + Refuse($"parameter '{parameter.Name}' must be a plain string"); + } + } + } + + private void CheckStatusShape() + { + var successes = _operation.Responses.Where(static response => response.StatusCode is >= 200 and < 300).ToArray(); + if (successes.Length is not 1) + { + Refuse("operation must declare exactly one success response"); + } + else if (successes[0].StatusCode is not 200) + { + Refuse("the success response must use status 200"); + } + + foreach (var response in _operation.Responses + .Where(static response => response.StatusCode is < 200 or (>= 300 and < 400))) + { + Refuse($"status '{response.StatusCode.ToString(CultureInfo.InvariantCulture)}' must be an error status"); + } + } + + private EnvelopePlan? BindEnvelope(SpecResponse success) + { + if (success.ContentType is not { IsJson: true } || success.Schema is null) + { + Refuse("the success response must carry a JSON schema"); + return null; + } + + var payload = success.EnvelopeShape switch + { + SpecEnvelopeShape.Bare => BindBarePayload(success.Schema), + SpecEnvelopeShape.Data => BindDataPayload(success.Schema), + SpecEnvelopeShape.None or SpecEnvelopeShape.DataLocation or SpecEnvelopeShape.CursorData + or SpecEnvelopeShape.DataHasMore + => RefuseNull($"envelope shape '{success.EnvelopeShape}' is not supported in M1"), + _ => RefuseNull($"envelope shape '{success.EnvelopeShape}' is not supported in M1"), + }; + if (payload is null) + { + return null; + } + + var responseTypeName = OperationNamePolicy.ResponseTypeName(_operation); + var payloadName = _curation.EnvelopePayloadNames.TryGetValue(_operation.OperationId, out var curated) + ? curated + : OperationNamePolicy.PayloadName(_operation); + if (ReservedPayloadNames.Contains(payloadName, StringComparer.Ordinal) + || string.Equals(payloadName, responseTypeName, StringComparison.Ordinal)) + { + _errors.Add( + BindingErrorCategory.Naming, + _operation.OperationId, + $"payload name '{payloadName}' collides with the response spine of '{responseTypeName}'"); + return null; + } + + return new EnvelopePlan + { + ResponseTypeName = responseTypeName, + AdapterTypeName = $"{responseTypeName}Adapter", + PayloadName = payloadName, + PayloadTypeName = payload, + HasDataEnvelope = success.EnvelopeShape is SpecEnvelopeShape.Data, + }; + } + + private string? BindBarePayload(SchemaNode schema) + { + return schema is RefNode reference && _typeNames.TryGetValue(reference.Target, out var name) + ? name + : RefuseNull("success payload must reference a named schema"); + } + + private string? BindDataPayload(SchemaNode schema) + { + if (schema is RefNode reference + && _document.Schemas.TryGetValue(reference.Target, out var target) + && target is ObjectNode { Properties: [{ Name: "data", IsRequired: true } data] } + && data.Schema is RefNode payload + && _typeNames.TryGetValue(payload.Target, out var name)) + { + return name; + } + + return RefuseNull("envelope payload must be a required reference to a named schema"); + } + + private ErrorMapPlan? BindErrorMap() + { + var statuses = new List(); + var complete = true; + foreach (var response in _operation.Responses.Where(static response => response.StatusCode is >= 400)) + { + var tags = BindErrorStatus(response); + if (tags is null) + { + complete = false; + continue; + } + + statuses.Add(new ErrorStatusPlan + { + StatusCode = response.StatusCode, + Tags = tags, + }); + } + + return complete + ? new ErrorMapPlan + { + Statuses = [.. statuses.OrderBy(static status => status.StatusCode)], + } + : null; + } + + private IReadOnlyList? BindErrorStatus(SpecResponse response) + { + if (response.ContentType is not { IsJson: true } || response.Schema is null) + { + return RefuseNullTags("error responses must carry a JSON schema"); + } + + var targets = ResolveErrorTargets(response.Schema); + if (targets is null) + { + return RefuseNullTags("error responses must reference Effect-tagged error schemas"); + } + + var tags = new List(targets.Count); + foreach (var (key, node) in targets) + { + var markers = node.LiteralMarkers.Where(static marker => marker.PropertyName is "_tag").ToArray(); + if (node.ErrorStyle is not ErrorStyle.EffectTag || markers is not [var marker]) + { + return RefuseNullTags("error responses must reference Effect-tagged error schemas"); + } + + if (!_typeNames.TryGetValue(key, out var typeName)) + { + _errors.Add(BindingErrorCategory.Naming, key, "error schema has no unique C# type name"); + return null; + } + + tags.Add(new ErrorTagPlan + { + Tag = marker.Value, + TypeName = typeName, + }); + } + + var duplicate = tags.GroupBy(static tag => tag.Tag, StringComparer.Ordinal).FirstOrDefault(static tag => tag.Skip(1).Any()); + if (duplicate is not null) + { + return RefuseNullTags($"status '{response.StatusCode.ToString(CultureInfo.InvariantCulture)}' declares duplicate error tag '{duplicate.Key}'"); + } + + return [.. tags.OrderBy(static tag => tag.Tag, StringComparer.Ordinal)]; + } + + /// Resolves an error response schema into the object schemas carrying its tags. + private List>? ResolveErrorTargets(SchemaNode schema) + { + if (schema is not RefNode reference || !_document.Schemas.TryGetValue(reference.Target, out var target)) + { + return null; + } + + switch (target) + { + case ObjectNode objectNode: + return [new KeyValuePair(reference.Target, objectNode)]; + case UnionNode { Classification: UnionClassification.Marked } union: + var branches = new List>(union.Branches.Count); + foreach (var branch in union.Branches) + { + if (branch is not RefNode branchReference + || !_document.Schemas.TryGetValue(branchReference.Target, out var branchTarget) + || branchTarget is not ObjectNode branchNode) + { + return null; + } + + branches.Add(new KeyValuePair(branchReference.Target, branchNode)); + } + + return branches; + default: + return null; + } + } + + private IReadOnlyList? BindParameters(GroupCuration row) + { + var plans = _operation.Parameters + .Select(parameter => new OperationParameterPlan + { + WireName = parameter.Name, + Name = CSharpNamePolicy.ToCamelCase(parameter.Name), + TypeName = "string", + IsHandleParameter = row.Placement is GroupPlacement.Client + && string.Equals(parameter.Name, row.HandleParameter, StringComparison.Ordinal), + }) + .ToList(); + + var duplicate = plans.GroupBy(static plan => plan.Name, StringComparer.Ordinal).FirstOrDefault(static plan => plan.Skip(1).Any()); + if (duplicate is not null) + { + _errors.Add( + BindingErrorCategory.Naming, + _operation.OperationId, + $"multiple parameters map to C# name '{duplicate.Key}'"); + return null; + } + + return [.. plans.OrderBy(plan => TemplatePosition(plan.WireName))]; + } + + private int TemplatePosition(string wireName) + { + var position = _operation.Path.IndexOf($"{{{wireName}}}", StringComparison.Ordinal); + Debug.Assert(position >= 0, "Ingestion guarantees every path parameter appears in the route template."); + return position; + } + + private void Refuse(string problem) => _errors.Add(BindingErrorCategory.Operation, _operation.OperationId, problem); + + private string? RefuseNull(string problem) + { + Refuse(problem); + return null; + } + + private IReadOnlyList? RefuseNullTags(string problem) + { + Refuse(problem); + return null; + } + + private SchemaNode Resolve(SchemaNode schema) + { + var visited = new HashSet(StringComparer.Ordinal); + var current = schema; + while (current is RefNode reference && visited.Add(reference.Target) + && _document.Schemas.TryGetValue(reference.Target, out var target)) + { + current = target; + } + + return current; + } + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs index 21448a7cb..ae97232d9 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs @@ -35,8 +35,7 @@ public ReachableSchemaSet Collect(SpecDocument document, IReadOnlyList graph, BindingErrorCollector errors, - StringComparer comparer) + private sealed class ReachabilityTraversal(IReadOnlyDictionary graph, BindingErrorCollector errors, StringComparer comparer) { private readonly StringComparer _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); private readonly BindingErrorCollector _errors = errors ?? throw new ArgumentNullException(nameof(errors)); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs index 98f370fda..88d64456d 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs @@ -8,8 +8,7 @@ internal sealed class SchemaNameResolver { private readonly StringComparer _comparer = StringComparer.Ordinal; - public IReadOnlyDictionary Resolve(SpecDocument document, ReachableSchemaSet reachable, - BindingErrorCollector errors) + public IReadOnlyDictionary Resolve(SpecDocument document, ReachableSchemaSet reachable, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(reachable); @@ -93,14 +92,28 @@ private static string ResolveDefault(string key) var index = 0; while (index < segments.Length - 1) { - if (!string.Equals(segments[index], "properties", StringComparison.Ordinal)) + if (string.Equals(segments[index], "properties", StringComparison.Ordinal)) { - index++; + _ = result.Append(CSharpNamePolicy.ToPascalCase(DecodePointer(segments[index + 1]))); + index += 2; continue; } - _ = result.Append(CSharpNamePolicy.ToPascalCase(DecodePointer(segments[index + 1]))); - index += 2; + // A promoted union branch is keyed by its marker ("anyOf/type=inline") or, for + // unmarked branches, by ordinal; the branch name appends the marker value so + // sibling branches (and their root) never collide: Prompt.FileSource#/anyOf/ + // type=inline -> PromptFileSourceInline. + if (string.Equals(segments[index], "anyOf", StringComparison.Ordinal) + || string.Equals(segments[index], "oneOf", StringComparison.Ordinal)) + { + var branch = DecodePointer(segments[index + 1]); + var separator = branch.IndexOf('=', StringComparison.Ordinal); + _ = result.Append(CSharpNamePolicy.ToPascalCase(separator >= 0 ? branch[(separator + 1)..] : branch)); + index += 2; + continue; + } + + index++; } return result.ToString(); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs index a7a20691b..e6d0e3e1e 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs @@ -3,21 +3,21 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class SchemaPlanBinder(SchemaNameResolver schemaNames) +internal sealed class SchemaPlanBinder { private const string ModelNamespace = "OpenCode.Sdk.Models"; - private readonly SchemaNameResolver _schemaNames = schemaNames ?? throw new ArgumentNullException(nameof(schemaNames)); + private readonly StringComparer _comparer = StringComparer.Ordinal; public SchemaBindingResult Bind(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation, - BindingErrorCollector errors) + IReadOnlyDictionary typeNames, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(reachable); ArgumentNullException.ThrowIfNull(curation); + ArgumentNullException.ThrowIfNull(typeNames); ArgumentNullException.ThrowIfNull(errors); - var typeNames = _schemaNames.Resolve(document, reachable, errors); - var responseRoots = reachable.ResponseRootKeys.ToHashSet(StringComparer.Ordinal); + var responseRoots = reachable.ResponseRootKeys.ToHashSet(_comparer); RefuseStructuralUnions(document, reachable, errors); var inheritance = new Dictionary(StringComparer.Ordinal); @@ -53,12 +53,12 @@ public SchemaBindingResult Bind(SpecDocument document, ReachableSchemaSet reacha } } - var orderedModels = models.OrderBy(static model => model.Name, StringComparer.Ordinal).ToArray(); - var orderedUnions = unions.OrderBy(static union => union.Name, StringComparer.Ordinal).ToArray(); + var orderedModels = models.OrderBy(static model => model.Name, _comparer).ToArray(); + var orderedUnions = unions.OrderBy(static union => union.Name, _comparer).ToArray(); var registryNames = orderedModels.Select(static model => model.Name) .Concat(orderedUnions.SelectMany(static union => new[] { union.Name, union.UnknownTypeName, })) - .Distinct(StringComparer.Ordinal) - .Order(StringComparer.Ordinal) + .Distinct(_comparer) + .Order(_comparer) .ToArray(); return new SchemaBindingResult { @@ -83,9 +83,10 @@ private static void RefuseStructuralUnions(SpecDocument document, ReachableSchem } private static List BindExplicitUnions(SpecDocument document, ReachableSchemaSet reachable, HashSet responseRoots, - IReadOnlyDictionary names, IDictionary inheritance, BindingErrorCollector errors) + IReadOnlyDictionary names, Dictionary inheritance, BindingErrorCollector errors) { - var result = new List(); + var plans = new Dictionary(StringComparer.Ordinal); + var fixedMarkers = new Dictionary(StringComparer.Ordinal); foreach (var key in reachable.GraphKeys) { if (responseRoots.Contains(key) @@ -96,70 +97,182 @@ private static List BindExplicitUnions(SpecDocument document, Reachab continue; } - var plan = BindUnion(name, key, union, document.Schemas, names, inheritance, errors); + var plan = BindUnion(name, key, union, document.Schemas, names, inheritance, fixedMarkers, errors); if (plan is not null) { - result.Add(plan); + plans.Add(key, plan); } } + // A nested union learns its base type and fixed outer marker while the outer union + // binds, which can happen after the nested plan was built — the wiring is a post-pass. + var result = new List(plans.Count); + foreach (var (key, plan) in plans) + { + result.Add(fixedMarkers.TryGetValue(key, out var fixedMarker) && inheritance.TryGetValue(key, out var baseType) + ? plan with { BaseTypeName = baseType, FixedMarker = fixedMarker } + : plan); + } + return result; } private static UnionPlan? BindUnion(string name, string key, UnionNode union, IReadOnlyDictionary graph, - IReadOnlyDictionary names, IDictionary inheritance, BindingErrorCollector errors) + IReadOnlyDictionary names, Dictionary inheritance, + Dictionary fixedMarkers, BindingErrorCollector errors) + { + var resolved = ResolveBranches(key, union, graph, names, errors); + if (resolved is null) + { + return null; + } + + var marker = SelectDiscriminatingMarker(resolved); + if (marker is null) + { + errors.Add(BindingErrorCategory.Schema, key, "marked union branches share no discriminating marker property"); + return null; + } + + var variants = new List(resolved.Count); + foreach (var branch in resolved) + { + var tag = branch.Markers.First(candidate => + string.Equals(candidate.PropertyName, marker.PropertyName, StringComparison.Ordinal)).Value; + AddInheritance(branch.TargetKey, name, inheritance, errors); + if (branch.IsNestedUnion) + { + fixedMarkers[branch.TargetKey] = new UnionFixedMarkerPlan + { + WireName = marker.PropertyName, + Name = CSharpNamePolicy.ToPascalCase(marker.PropertyName), + Kind = marker.Kind, + Value = tag, + }; + } + + variants.Add(new UnionVariantPlan + { + TypeName = branch.TypeName, + Tag = tag, + IsNestedUnion = branch.IsNestedUnion, + }); + } + + return new UnionPlan + { + Name = name, + Namespace = ModelNamespace, + UnknownTypeName = $"Unknown{name}", + MarkerWireName = marker.PropertyName, + MarkerName = CSharpNamePolicy.ToPascalCase(marker.PropertyName), + MarkerKind = marker.Kind, + Variants = variants, + Description = union.Description, + }; + } + + private static List? ResolveBranches(string key, UnionNode union, IReadOnlyDictionary graph, + IReadOnlyDictionary names, BindingErrorCollector errors) { - var variants = new List(union.Branches.Count); - LiteralMarker? expectedMarker = null; - var tags = new HashSet(StringComparer.Ordinal); + var resolved = new List(union.Branches.Count); foreach (var branch in union.Branches) { if (branch is not RefNode reference || !graph.TryGetValue(reference.Target, out var target) - || target is not ObjectNode { LiteralMarkers.Count: > 0 } objectNode || !names.TryGetValue(reference.Target, out var typeName)) { - errors.Add(BindingErrorCategory.Schema, key, "marked union branch must reference a named object with a literal marker"); + errors.Add( + BindingErrorCategory.Schema, + key, + "marked union branch must reference a named object or nested marked union with a literal marker"); continue; } - var marker = objectNode.LiteralMarkers[0]; - expectedMarker ??= marker; - if (!string.Equals(marker.PropertyName, expectedMarker.PropertyName, StringComparison.Ordinal) || marker.Kind != expectedMarker.Kind) + var markers = target switch { - errors.Add(BindingErrorCategory.Schema, key, "marked union branches do not share one marker property and kind"); + ObjectNode { LiteralMarkers.Count: > 0 } objectNode => objectNode.LiteralMarkers, + UnionNode { Classification: UnionClassification.Marked } nested => ResolveUniformNestedMarkers(nested, graph), + _ => null, + }; + if (markers is not { Count: > 0 }) + { + errors.Add( + BindingErrorCategory.Schema, + key, + "marked union branch must reference a named object or nested marked union with a literal marker"); continue; } - if (!tags.Add(marker.Value)) + resolved.Add(new ResolvedUnionBranch(reference.Target, typeName, markers, target is UnionNode)); + } + + return resolved.Count == union.Branches.Count ? resolved : null; + } + + /// + /// Resolves the markers every variant of a nested union fixes to one shared value — for + /// those properties the whole nested union behaves like a single tag. + /// + private static IReadOnlyList? ResolveUniformNestedMarkers(UnionNode nested, + IReadOnlyDictionary graph) + { + var variantMarkers = new List>(nested.Branches.Count); + foreach (var branch in nested.Branches) + { + if (branch is not RefNode reference + || !graph.TryGetValue(reference.Target, out var target) + || target is not ObjectNode { LiteralMarkers.Count: > 0 } objectNode) { - errors.Add(BindingErrorCategory.Schema, key, $"marked union tag '{marker.Value}' is duplicated"); - continue; + return null; } - AddInheritance(reference.Target, name, inheritance, errors); - variants.Add(new UnionVariantPlan - { - TypeName = typeName, - Tag = marker.Value, - }); + variantMarkers.Add(objectNode.LiteralMarkers); } - return expectedMarker is null || variants.Count != union.Branches.Count - ? null - : new UnionPlan + return + [ + .. variantMarkers[0] + .Where(candidate => variantMarkers.Skip(1).All(markers => markers.Any(other => + string.Equals(other.PropertyName, candidate.PropertyName, StringComparison.Ordinal) + && other.Kind == candidate.Kind + && string.Equals(other.Value, candidate.Value, StringComparison.Ordinal)))), + ]; + } + + /// + /// Selects the first property (in the first branch's document order) that every branch + /// carries with one kind and a value distinct from every other branch. + /// + private static LiteralMarker? SelectDiscriminatingMarker(IReadOnlyList branches) + { + foreach (var candidate in branches[0].Markers) + { + var values = new HashSet(StringComparer.Ordinal); + var qualified = true; + foreach (var branch in branches) { - Name = name, - Namespace = ModelNamespace, - UnknownTypeName = $"Unknown{name}", - MarkerWireName = expectedMarker.PropertyName, - MarkerName = CSharpNamePolicy.ToPascalCase(expectedMarker.PropertyName), - MarkerKind = expectedMarker.Kind, - Variants = variants, - Description = union.Description, - }; + var match = branch.Markers.FirstOrDefault(marker => + string.Equals(marker.PropertyName, candidate.PropertyName, StringComparison.Ordinal) + && marker.Kind == candidate.Kind); + if (match is null || !values.Add(match.Value)) + { + qualified = false; + break; + } + } + + if (qualified) + { + return candidate; + } + } + + return null; } + private sealed record ResolvedUnionBranch(string TargetKey, string TypeName, IReadOnlyList Markers, bool IsNestedUnion); + private static UnionPlan? BindErrorUnion(SpecDocument document, ReachableSchemaSet reachable, HashSet responseRoots, IReadOnlyDictionary names, IDictionary inheritance, BindingErrorCollector errors) { @@ -259,8 +372,10 @@ private static List BindExplicitUnions(SpecDocument document, Reachab return null; } - var duplicate = properties.GroupBy(static property => property.Name, StringComparer.Ordinal) - .FirstOrDefault(static group => group.Count() > 1); + var duplicate = properties + .GroupBy(static property => property.Name, StringComparer.Ordinal) + .FirstOrDefault(static group => group.Skip(1).Any()); + if (duplicate is not null) { errors.Add(BindingErrorCategory.Naming, key, $"multiple properties map to C# name '{duplicate.Key}'"); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs index 97d329a3d..6cbacb5a7 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using OpenCode.Sdk.Tools.Generator.Binding.Abstractions; using OpenCode.Sdk.Tools.Generator.Binding.Models; using OpenCode.Sdk.Tools.Generator.Ingestion.Models; @@ -7,10 +8,14 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; internal sealed class SpecBinder( ReachableSchemaCollector reachableSchemas, CurationValidator curationValidator, - SchemaPlanBinder schemaPlans) : ISpecBinder + SchemaNameResolver schemaNames, + SchemaPlanBinder schemaPlans, + OperationPlanBinder operationPlans) : ISpecBinder { private readonly CurationValidator _curationValidator = curationValidator ?? throw new ArgumentNullException(nameof(curationValidator)); + private readonly OperationPlanBinder _operationPlans = operationPlans ?? throw new ArgumentNullException(nameof(operationPlans)); private readonly ReachableSchemaCollector _reachableSchemas = reachableSchemas ?? throw new ArgumentNullException(nameof(reachableSchemas)); + private readonly SchemaNameResolver _schemaNames = schemaNames ?? throw new ArgumentNullException(nameof(schemaNames)); private readonly SchemaPlanBinder _schemaPlans = schemaPlans ?? throw new ArgumentNullException(nameof(schemaPlans)); public EmitPlan Bind(SpecDocument document, OperationSelection selection, GenerationCuration curation) @@ -24,16 +29,19 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera var selected = SelectOperations(selection, operationsById, errors); var reachable = _reachableSchemas.Collect(document, selected, errors); _curationValidator.Validate(document, selected, reachable, curation, errors); - var schemaResult = _schemaPlans.Bind(document, reachable, curation, errors); + + // Type names are resolved exactly once per bind; schema and operation binding + // consume the same map so neither can reinterpret a schema under another name. + var typeNames = _schemaNames.Resolve(document, reachable, errors); + var schemaResult = _schemaPlans.Bind(document, reachable, curation, typeNames, errors); + var clients = _operationPlans.Bind(document, selected, curation, typeNames, errors); var selectedIds = selected.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal); var pending = document.Operations .Where(operation => !selectedIds.Contains(operation.OperationId)) - .OrderBy(static operation => operation.Surface) - .ThenBy(static operation => operation.OperationId, StringComparer.Ordinal) + .OrderBy(static operation => operation.OperationId, StringComparer.Ordinal) .Select(static operation => new PendingOperationPlan { OperationId = operation.OperationId, - Surface = operation.Surface, }) .ToArray(); @@ -44,11 +52,12 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera Models = schemaResult.Models, Unions = schemaResult.Unions, Registry = schemaResult.Registry, + Clients = clients, PendingOperations = pending, }; } - private static System.Collections.ObjectModel.ReadOnlyCollection SelectOperations(OperationSelection selection, + private static ReadOnlyCollection SelectOperations(OperationSelection selection, Dictionary operationsById, BindingErrorCollector errors) { var selected = new List(selection.OperationIds.Count); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/ClientEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/ClientEmitter.cs new file mode 100644 index 000000000..36ee02ad8 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/ClientEmitter.cs @@ -0,0 +1,336 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using OpenCode.Sdk.Tools.Generator.Binding; +using OpenCode.Sdk.Tools.Generator.Binding.Models; + +namespace OpenCode.Sdk.Tools.Generator.Emission; + +internal static class ClientEmitter +{ + public static IReadOnlyList Emit(IReadOnlyList clients) + { + ArgumentNullException.ThrowIfNull(clients); + + return Array.AsReadOnly([.. clients.Select(static client => EmitClient(client))]); + } + + private static GeneratedSource EmitClient(ClientPlan client) + { + var members = new List(); + members.AddRange(EmitFields(client)); + members.AddRange(client.Role switch + { + ClientRole.Root => EmitRootConstructors(client), + ClientRole.Collection => EmitPipelineConstructor(client, EmitPipelineAssignments(client)), + ClientRole.Handle => EmitHandleConstructor(client), + _ => throw new InvalidOperationException($"Unknown client role '{client.Role}'."), + }); + members.Add(EmitMockConstructor(client)); + if (client.Role is ClientRole.Root) + { + members.AddRange(EmitDispose()); + } + + members.AddRange(client.SubClients.Select(reference => EmitSubClientProperty(client, reference))); + if (client.HandleFactory is not null) + { + members.Add(EmitHandleFactory(client, client.HandleFactory)); + } + + members.Add(EmitGuardedAccessor(client, "Pipeline", "Pipeline", "_pipeline")); + if (client.HandleParameter is not null) + { + members.Add(EmitGuardedAccessor( + client, + CSharpNamePolicy.ToPascalCase(client.HandleParameter.Name), + client.HandleParameter.TypeName, + HandleFieldName(client.HandleParameter))); + } + + members.AddRange(client.Operations.Select(OperationMethodEmitter.Emit)); + + var declaration = SyntaxFactory.ClassDeclaration(client.Name) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) + .WithMembers(SyntaxFactory.List(members)) + .WithLeadingTrivia(EmissionSyntax.Documentation(Summary(client))); + if (client.Role is ClientRole.Root) + { + declaration = declaration.WithBaseList(SyntaxFactory.BaseList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.SimpleBaseType(TypeSyntaxEmitter.EmitNamed("IDisposable"))))); + } + + var unit = EmissionSyntax.CompilationUnit( + client.Namespace, + [ + "System", + "System.Net.Http", + "System.Threading", + "System.Threading.Tasks", + "OpenCode.Sdk.Internal", + "OpenCode.Sdk.Internal.ResponseAdapters", + ], + [declaration]); + return EmissionSyntax.CreateSource($"{client.Name}.cs", unit); + } + + private static string Summary(ClientPlan client) => client.Role switch + { + ClientRole.Root => "The opencode API client and composition root.", + ClientRole.Collection => $"The '{client.Name}' collection client.", + ClientRole.Handle => $"A bound '{client.Name}' handle; it holds an immutable identifier and the shared pipeline.", + _ => throw new InvalidOperationException($"Unknown client role '{client.Role}'."), + }; + + private static IEnumerable EmitFields(ClientPlan client) + { + yield return EmitField("_pipeline", TypeSyntaxEmitter.EmitNamed("Pipeline")); + if (client.HandleParameter is not null) + { + yield return EmitField(HandleFieldName(client.HandleParameter), TypeSyntaxEmitter.EmitNamed(client.HandleParameter.TypeName)); + } + + foreach (var reference in client.SubClients) + { + yield return EmitField(SubClientFieldName(reference), TypeSyntaxEmitter.EmitNamed(reference.TypeName)); + } + } + + private static FieldDeclarationSyntax EmitField(string name, TypeSyntax type) => + SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration( + SyntaxFactory.NullableType(type), + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.VariableDeclarator(name)))) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PrivateKeyword), + SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword))); + + private static IEnumerable EmitRootConstructors(ClientPlan client) + { + yield return SyntaxFactory.ConstructorDeclaration(client.Name) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("endpoint")).WithType(TypeSyntaxEmitter.EmitNamed("Uri"))))) + .WithInitializer(SyntaxFactory.ConstructorInitializer( + SyntaxKind.ThisConstructorInitializer, + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("endpoint")), + SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)) + .WithNameColon(SyntaxFactory.NameColon("options")), + ])))) + .WithBody(SyntaxFactory.Block()) + .WithLeadingTrivia(EmissionSyntax.MemberDocumentation( + "Initializes a client that owns its connection to the endpoint.", + [new DocumentedParameter("endpoint", "The absolute HTTP or HTTPS server endpoint.")])); + yield return SyntaxFactory.ConstructorDeclaration(client.Name) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Parameter(SyntaxFactory.Identifier("endpoint")).WithType(TypeSyntaxEmitter.EmitNamed("Uri")), + SyntaxFactory.Parameter(SyntaxFactory.Identifier("options")) + .WithType(SyntaxFactory.NullableType(TypeSyntaxEmitter.EmitNamed("OpenCodeClientOptions"))), + ]))) + .WithBody(SyntaxFactory.Block(EmitRootAssignments(client, "endpoint"))) + .WithLeadingTrivia(EmissionSyntax.MemberDocumentation( + "Initializes a client that owns its connection to the endpoint.", + [ + new DocumentedParameter("endpoint", "The absolute HTTP or HTTPS server endpoint."), + new DocumentedParameter("options", "The client options; its endpoint must stay unset on this path."), + ])); + yield return SyntaxFactory.ConstructorDeclaration(client.Name) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Parameter(SyntaxFactory.Identifier("httpClient")).WithType(TypeSyntaxEmitter.EmitNamed("HttpClient")), + SyntaxFactory.Parameter(SyntaxFactory.Identifier("options")).WithType(TypeSyntaxEmitter.EmitNamed("OpenCodeClientOptions")), + ]))) + .WithBody(SyntaxFactory.Block(EmitRootAssignments(client, "httpClient"))) + .WithLeadingTrivia(EmissionSyntax.MemberDocumentation( + "Initializes a client over a caller-owned HttpClient; the SDK never disposes it.", + [ + new DocumentedParameter("httpClient", "The caller-owned HTTP client."), + new DocumentedParameter("options", "The client options; the endpoint is required on this path."), + ])); + } + + private static List EmitRootAssignments(ClientPlan client, string firstArgument) + { + var statements = new List + { + SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("_pipeline"), + EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("Pipeline"), "Create"), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName(firstArgument)), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("options")), + SyntaxFactory.Argument(SyntaxFactory.ObjectCreationExpression( + TypeSyntaxEmitter.EmitNamed("SystemEnvironmentProvider")) + .WithArgumentList(SyntaxFactory.ArgumentList()))))), + }; + statements.AddRange(client.SubClients.Select(static reference => (StatementSyntax)SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName(SubClientFieldName(reference)), + SyntaxFactory.ObjectCreationExpression(TypeSyntaxEmitter.EmitNamed(reference.TypeName)) + .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("_pipeline"))))))))); + return statements; + } + + private static IEnumerable EmitPipelineConstructor(ClientPlan client, + IReadOnlyList assignments) + { + var statements = new List(); + statements.AddRange(EmissionSyntax.ArgumentNullGuard("pipeline")); + statements.AddRange(assignments); + yield return SyntaxFactory.ConstructorDeclaration(client.Name) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.InternalKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("pipeline")).WithType(TypeSyntaxEmitter.EmitNamed("Pipeline"))))) + .WithBody(SyntaxFactory.Block(statements)); + } + + private static IReadOnlyList EmitPipelineAssignments(ClientPlan client) + { + _ = client; + return + [ + SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("_pipeline"), + SyntaxFactory.IdentifierName("pipeline"))), + ]; + } + + private static IEnumerable EmitHandleConstructor(ClientPlan client) + { + var parameter = client.HandleParameter + ?? throw new InvalidOperationException($"Handle client '{client.Name}' carries no handle parameter."); + var statements = new List(); + statements.AddRange(EmissionSyntax.ArgumentNullGuard("pipeline")); + statements.AddRange(EmissionSyntax.ArgumentNullOrEmptyGuard(parameter.Name)); + statements.Add(SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("_pipeline"), + SyntaxFactory.IdentifierName("pipeline")))); + statements.Add(SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName(HandleFieldName(parameter)), + SyntaxFactory.IdentifierName(parameter.Name)))); + yield return SyntaxFactory.ConstructorDeclaration(client.Name) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.InternalKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Parameter(SyntaxFactory.Identifier("pipeline")).WithType(TypeSyntaxEmitter.EmitNamed("Pipeline")), + SyntaxFactory.Parameter(SyntaxFactory.Identifier(parameter.Name)) + .WithType(TypeSyntaxEmitter.EmitNamed(parameter.TypeName)), + ]))) + .WithBody(SyntaxFactory.Block(statements)); + } + + private static ConstructorDeclarationSyntax EmitMockConstructor(ClientPlan client) => + SyntaxFactory.ConstructorDeclaration(client.Name) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword))) + .WithBody(SyntaxFactory.Block()) + .WithLeadingTrivia(EmissionSyntax.Documentation( + "Initializes a mocking instance; members invoked without an override throw an instructive failure.")); + + private static IEnumerable EmitDispose() + { + yield return SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + "Dispose") + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) + .WithBody(SyntaxFactory.Block( + SyntaxFactory.ExpressionStatement(EmissionSyntax.Invocation( + SyntaxFactory.IdentifierName("Dispose"), + SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression)) + .WithNameColon(SyntaxFactory.NameColon("disposing")))), + SyntaxFactory.ExpressionStatement(EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("GC"), "SuppressFinalize"), + SyntaxFactory.Argument(SyntaxFactory.ThisExpression()))))) + .WithLeadingTrivia(EmissionSyntax.Documentation("Releases the owned transport resources.")); + yield return SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + "Dispose") + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.ProtectedKeyword), + SyntaxFactory.Token(SyntaxKind.VirtualKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("disposing")) + .WithType(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.BoolKeyword)))))) + .WithBody(SyntaxFactory.Block(SyntaxFactory.IfStatement( + SyntaxFactory.IdentifierName("disposing"), + SyntaxFactory.Block(SyntaxFactory.ExpressionStatement(SyntaxFactory.ConditionalAccessExpression( + SyntaxFactory.IdentifierName("_pipeline"), + EmissionSyntax.Invocation(SyntaxFactory.MemberBindingExpression(SyntaxFactory.IdentifierName("Dispose"))))))))) + .WithLeadingTrivia(EmissionSyntax.MemberDocumentation( + "Releases the owned transport resources.", + [new DocumentedParameter("disposing", "Whether managed state should be released.")])); + } + + private static PropertyDeclarationSyntax EmitSubClientProperty(ClientPlan client, ClientReferencePlan reference) => + SyntaxFactory.PropertyDeclaration(TypeSyntaxEmitter.EmitNamed(reference.TypeName), reference.PropertyName) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.VirtualKeyword))) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause( + GuardedRead(client, SubClientFieldName(reference), reference.PropertyName))) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) + .WithLeadingTrivia(EmissionSyntax.Documentation($"Gets the '{reference.PropertyName}' collection client.")); + + private static MethodDeclarationSyntax EmitHandleFactory(ClientPlan client, HandleFactoryPlan factory) + { + var statements = new List(); + statements.AddRange(EmissionSyntax.ArgumentNullOrEmptyGuard(factory.Parameter.Name)); + statements.Add(SyntaxFactory.ReturnStatement(SyntaxFactory.ObjectCreationExpression( + TypeSyntaxEmitter.EmitNamed(factory.HandleTypeName)) + .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("Pipeline")), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName(factory.Parameter.Name)), + ]))))); + _ = client; + return SyntaxFactory.MethodDeclaration(TypeSyntaxEmitter.EmitNamed(factory.HandleTypeName), factory.MethodName) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.VirtualKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Parameter(SyntaxFactory.Identifier(factory.Parameter.Name)) + .WithType(TypeSyntaxEmitter.EmitNamed(factory.Parameter.TypeName))))) + .WithBody(SyntaxFactory.Block(statements)) + .WithLeadingTrivia(EmissionSyntax.MemberDocumentation( + $"Gets a bound '{factory.HandleTypeName}'; the handle never caches server state.", + [new DocumentedParameter(factory.Parameter.Name, $"The '{factory.Parameter.WireName}' route value.")], + $"The bound '{factory.HandleTypeName}'.")); + } + + private static PropertyDeclarationSyntax EmitGuardedAccessor(ClientPlan client, string propertyName, string typeName, + string fieldName) + { + var declaration = SyntaxFactory.PropertyDeclaration(TypeSyntaxEmitter.EmitNamed(typeName), propertyName) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PrivateKeyword))) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(GuardedRead(client, fieldName, propertyName))) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + return declaration; + } + + private static BinaryExpressionSyntax GuardedRead(ClientPlan client, string fieldName, string memberName) => + SyntaxFactory.BinaryExpression( + SyntaxKind.CoalesceExpression, + SyntaxFactory.IdentifierName(fieldName), + SyntaxFactory.ThrowExpression(EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("MockSeam"), "CreateError"), + SyntaxFactory.Argument(SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(client.Name))), + SyntaxFactory.Argument(SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(memberName)))))); + + private static string SubClientFieldName(ClientReferencePlan reference) => + $"_{CSharpNamePolicy.ToCamelCase(reference.PropertyName)}"; + + private static string HandleFieldName(OperationParameterPlan parameter) => $"_{parameter.Name}"; +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/DocumentedException.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/DocumentedException.cs new file mode 100644 index 000000000..6d8125f56 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/DocumentedException.cs @@ -0,0 +1,3 @@ +namespace OpenCode.Sdk.Tools.Generator.Emission; + +internal sealed record DocumentedException(string TypeName, string Text); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/DocumentedParameter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/DocumentedParameter.cs new file mode 100644 index 000000000..74680f2a1 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/DocumentedParameter.cs @@ -0,0 +1,3 @@ +namespace OpenCode.Sdk.Tools.Generator.Emission; + +internal sealed record DocumentedParameter(string Name, string Text); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs index 74d130fa9..493f54eea 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs @@ -18,7 +18,7 @@ public static GeneratedSource CreateSource(string relativePath, CompilationUnitS var normalized = compilationUnit.NormalizeWhitespace(indentation: " ", eol: "\n", elasticTrivia: false).ToFullString(); if (normalized.Length is 0 || normalized[^1] is not '\n') { - normalized = string.Concat(normalized, "\n"); + normalized = $"{normalized}\n"; } return new GeneratedSource @@ -46,9 +46,9 @@ public static CompilationUnitSyntax CompilationUnit(string namespaceName, IReadO .WithMembers(SyntaxFactory.SingletonList(namespaceDeclaration)); return compilationUnit.WithLeadingTrivia( - SyntaxFactory.Comment("// Generated by OpenCode.Sdk.Tools from spec/openapi.json."), + SyntaxFactory.Comment(GenerationProvenance.FirstLine), LineFeed, - SyntaxFactory.Comment("// Do not edit by hand — change tools/curation.json or the emitters, then regenerate."), + SyntaxFactory.Comment(GenerationProvenance.SecondLine), LineFeed, LineFeed); } @@ -70,10 +70,38 @@ public static SyntaxTriviaList Documentation(string summary) { ArgumentException.ThrowIfNullOrWhiteSpace(summary); var normalized = NormalizeDocumentation(summary); - var text = string.Concat("/// \n/// ", EscapeXml(normalized), "\n/// \n"); + var text = $"/// \n/// {EscapeXml(normalized)}\n/// \n"; return SyntaxFactory.ParseLeadingTrivia(text); } + public static SyntaxTriviaList MemberDocumentation(string summary, IReadOnlyList parameters, + string? returns = null, IReadOnlyList? exceptions = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(summary); + ArgumentNullException.ThrowIfNull(parameters); + + var text = new StringBuilder() + .Append("/// \n/// ").Append(EscapeXml(NormalizeDocumentation(summary))).Append("\n/// \n"); + foreach (var parameter in parameters) + { + _ = text.Append("/// ") + .Append(EscapeXml(NormalizeDocumentation(parameter.Text))).Append("\n"); + } + + if (returns is not null) + { + _ = text.Append("/// ").Append(EscapeXml(NormalizeDocumentation(returns))).Append("\n"); + } + + foreach (var exception in exceptions ?? []) + { + _ = text.Append("/// ") + .Append(EscapeXml(NormalizeDocumentation(exception.Text))).Append("\n"); + } + + return SyntaxFactory.ParseLeadingTrivia(text.ToString()); + } + public static AttributeListSyntax Attribute(string name, params AttributeArgumentSyntax[] arguments) { ArgumentException.ThrowIfNullOrWhiteSpace(name); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/EnvelopeEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/EnvelopeEmitter.cs new file mode 100644 index 000000000..62d4d7534 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/EnvelopeEmitter.cs @@ -0,0 +1,189 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using OpenCode.Sdk.Tools.Generator.Binding; +using OpenCode.Sdk.Tools.Generator.Binding.Models; + +namespace OpenCode.Sdk.Tools.Generator.Emission; + +internal static class EnvelopeEmitter +{ + public static IReadOnlyList Emit(IReadOnlyList clients) + { + ArgumentNullException.ThrowIfNull(clients); + + return Array.AsReadOnly( + [ + .. clients + .SelectMany(static client => client.Operations) + .OrderBy(static operation => operation.Envelope.ResponseTypeName, StringComparer.Ordinal) + .Select(static operation => EmitEnvelope(operation)), + ]); + } + + private static GeneratedSource EmitEnvelope(OperationPlan operation) + { + var envelope = operation.Envelope; + var fieldName = $"_{CSharpNamePolicy.ToCamelCase(envelope.PayloadName)}"; + var declaration = SyntaxFactory.RecordDeclaration(SyntaxFactory.Token(SyntaxKind.RecordKeyword), envelope.ResponseTypeName) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.SealedKeyword))) + .WithBaseList(SyntaxFactory.BaseList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.SimpleBaseType(TypeSyntaxEmitter.EmitNamed("OpenCodeResponse"))))) + .WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken)) + .WithMembers(SyntaxFactory.List( + [ + EmitBackingField(fieldName, envelope.PayloadTypeName), + EmitSuccessConstructor(envelope.ResponseTypeName), + EmitErrorConstructor(envelope), + EmitPayloadProperty(envelope, fieldName), + EmitPrintMembers(envelope.PayloadName, fieldName), + ])) + .WithLeadingTrivia(EmissionSyntax.Documentation( + $"Represents the response of the '{operation.HttpMethod.ToUpperInvariant()} {operation.RouteTemplate}' operation.")); + var unit = EmissionSyntax.CompilationUnit( + "OpenCode.Sdk", + ["System", "System.Diagnostics.CodeAnalysis", "System.Text", "OpenCode.Sdk.Models"], + [declaration]); + return EmissionSyntax.CreateSource($"{envelope.ResponseTypeName}.cs", unit); + } + + private static FieldDeclarationSyntax EmitBackingField(string fieldName, string payloadTypeName) => + SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration( + SyntaxFactory.NullableType(TypeSyntaxEmitter.EmitNamed(payloadTypeName)), + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.VariableDeclarator(fieldName)))) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PrivateKeyword), + SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword))); + + private static ConstructorDeclarationSyntax EmitSuccessConstructor(string responseTypeName) => + SyntaxFactory.ConstructorDeclaration(responseTypeName) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) + .WithBody(SyntaxFactory.Block()) + .WithLeadingTrivia(EmissionSyntax.Documentation( + $"Initializes a success instance of the '{responseTypeName}' envelope.")); + + private static ConstructorDeclarationSyntax EmitErrorConstructor(EnvelopePlan envelope) + { + var responseTypeName = envelope.ResponseTypeName; + + // The payload assignment is the SDK's single null-forgiveness: the guarded getter + // makes the null unobservable. + var assignments = new StatementSyntax[] + { + Assign("Status", SyntaxFactory.IdentifierName("status")), + Assign("IsError", SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression)), + Assign("Error", SyntaxFactory.IdentifierName("error")), + Assign("RawBody", SyntaxFactory.IdentifierName("rawBody")), + Assign(envelope.PayloadName, SyntaxFactory.PostfixUnaryExpression( + SyntaxKind.SuppressNullableWarningExpression, + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))), + }; + return SyntaxFactory.ConstructorDeclaration(responseTypeName) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.InternalKeyword))) + .AddAttributeLists(EmissionSyntax.Attribute("SetsRequiredMembers")) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Parameter(SyntaxFactory.Identifier("status")) + .WithType(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword))), + SyntaxFactory.Parameter(SyntaxFactory.Identifier("error")) + .WithType(SyntaxFactory.NullableType(TypeSyntaxEmitter.EmitNamed("OpenCodeError"))), + SyntaxFactory.Parameter(SyntaxFactory.Identifier("rawBody")) + .WithType(SyntaxFactory.NullableType(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)))), + ]))) + .WithBody(SyntaxFactory.Block(assignments)) + .WithLeadingTrivia(EmissionSyntax.Documentation( + "Initializes an error-path instance; the payload stays unset behind its guard.")); + } + + private static PropertyDeclarationSyntax EmitPayloadProperty(EnvelopePlan envelope, string fieldName) + { + var guardMessage = $"The response is an error; check IsError before accessing {envelope.PayloadName}."; + var getter = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.BinaryExpression( + SyntaxKind.CoalesceExpression, + SyntaxFactory.IdentifierName(fieldName), + SyntaxFactory.ThrowExpression(SyntaxFactory.ObjectCreationExpression( + TypeSyntaxEmitter.EmitNamed("InvalidOperationException")) + .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(guardMessage)))))))))) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + var setter = SyntaxFactory.AccessorDeclaration(SyntaxKind.InitAccessorDeclaration) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName(fieldName), + SyntaxFactory.IdentifierName("value")))) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + return SyntaxFactory.PropertyDeclaration(TypeSyntaxEmitter.EmitNamed(envelope.PayloadTypeName), envelope.PayloadName) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.RequiredKeyword))) + .WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List([getter, setter]))) + .WithLeadingTrivia(EmissionSyntax.Documentation( + $"Gets the {envelope.PayloadName} payload; guarded on the error path.")); + } + + private static MethodDeclarationSyntax EmitPrintMembers(string payloadName, string fieldName) + { + var payloadField = SyntaxFactory.IdentifierName(fieldName); + var builder = SyntaxFactory.IdentifierName("builder"); + var statements = new List(); + statements.AddRange(EmissionSyntax.ArgumentNullGuard("builder")); + statements.Add(SyntaxFactory.LocalDeclarationStatement(SyntaxFactory.VariableDeclaration( + SyntaxFactory.IdentifierName("var"), + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.VariableDeclarator("printed") + .WithInitializer(SyntaxFactory.EqualsValueClause(EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(SyntaxFactory.BaseExpression(), "PrintMembers"), + SyntaxFactory.Argument(builder)))))))); + statements.Add(SyntaxFactory.IfStatement( + SyntaxFactory.IsPatternExpression( + payloadField, + SyntaxFactory.ConstantPattern(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))), + SyntaxFactory.Block(SyntaxFactory.ReturnStatement(SyntaxFactory.IdentifierName("printed"))))); + statements.Add(SyntaxFactory.IfStatement( + SyntaxFactory.IdentifierName("printed"), + SyntaxFactory.Block(Discard(EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(builder, "Append"), + SyntaxFactory.Argument(SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(", ")))))))); + statements.Add(Discard(EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess( + EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(builder, "Append"), + SyntaxFactory.Argument(SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal($"{payloadName} = ")))), + "Append"), + SyntaxFactory.Argument(payloadField)))); + statements.Add(SyntaxFactory.ReturnStatement(SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression))); + return SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.BoolKeyword)), + "PrintMembers") + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.ProtectedKeyword), + SyntaxFactory.Token(SyntaxKind.OverrideKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("builder")) + .WithType(TypeSyntaxEmitter.EmitNamed("StringBuilder"))))) + .WithBody(SyntaxFactory.Block(statements)) + .WithLeadingTrivia(EmissionSyntax.Documentation( + "Prints the shared metadata and appends the payload only when it is present.")); + } + + private static ExpressionStatementSyntax Assign(string target, ExpressionSyntax value) => + SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName(target), + value)); + + private static ExpressionStatementSyntax Discard(ExpressionSyntax expression) => + SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("_"), + expression)); +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/GenerationProvenance.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/GenerationProvenance.cs new file mode 100644 index 000000000..571e2db5f --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/GenerationProvenance.cs @@ -0,0 +1,22 @@ +namespace OpenCode.Sdk.Tools.Generator.Emission; + +/// The header every generated source carries; the writer refuses files without it. +internal static class GenerationProvenance +{ + public const string FirstLine = "// Generated by OpenCode.Sdk.Tools from spec/openapi.json."; + public const string SecondLine = "// Do not edit by hand — change tools/curation.json or the emitters, then regenerate."; + public const string Header = $"{FirstLine}\n{SecondLine}\n"; + + /// Checks whether the content opens with the exact provenance header, tolerating CRLF. + /// The decoded file content. + /// Whether the header is present. + public static bool HasHeader(string content) + { + ArgumentNullException.ThrowIfNull(content); + + var lines = content.Split('\n'); + return lines.Length >= 2 + && string.Equals(lines[0].TrimEnd('\r'), FirstLine, StringComparison.Ordinal) + && string.Equals(lines[1].TrimEnd('\r'), SecondLine, StringComparison.Ordinal); + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/ModelEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/ModelEmitter.cs index 3bd9b46df..42500ee3e 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/ModelEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/ModelEmitter.cs @@ -18,7 +18,7 @@ public static IReadOnlyList Emit(EmitPlan plan) { result.Add(model switch { - ObjectModelPlan objectModel => EmitObject(objectModel, ResolveBaseUnion(objectModel, unions)), + ObjectModelPlan objectModel => EmitObject(objectModel, ResolveBaseUnion(objectModel, unions), unions), EnumModelPlan enumModel => EmitEnum(enumModel), _ => throw new InvalidOperationException($"Unknown model plan '{model.GetType().Name}'."), }); @@ -27,11 +27,16 @@ public static IReadOnlyList Emit(EmitPlan plan) return Array.AsReadOnly([.. result]); } - private static GeneratedSource EmitObject(ObjectModelPlan model, UnionPlan? baseUnion) + private static GeneratedSource EmitObject(ObjectModelPlan model, UnionPlan? baseUnion, IReadOnlyDictionary unions) { - if (baseUnion is not null && model.Properties.Count(property => IsDiscriminator(property, baseUnion)) is not 1) + // A variant overrides one abstract marker per level of its union chain: a nested + // union's variant carries both its own tag and the fixed outer tag. + var chainMarkers = GetChainMarkerWireNames(baseUnion, unions); + var unmatched = chainMarkers.Find(wireName => + model.Properties.Count(property => IsDiscriminator(property, wireName)) is not 1); + if (unmatched is not null) { - throw new InvalidOperationException($"Union variant '{model.Name}' must carry exactly one '{baseUnion.MarkerWireName}' marker."); + throw new InvalidOperationException($"Union variant '{model.Name}' must carry exactly one '{unmatched}' marker."); } var declaration = SyntaxFactory.RecordDeclaration(SyntaxFactory.Token(SyntaxKind.RecordKeyword), model.Name) @@ -41,7 +46,7 @@ private static GeneratedSource EmitObject(ObjectModelPlan model, UnionPlan? base .WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken)) .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken)) .WithMembers(SyntaxFactory.List(model.Properties.Select(property => - EmitProperty(property, baseUnion is not null && IsDiscriminator(property, baseUnion))))) + EmitProperty(property, chainMarkers.Any(wireName => IsDiscriminator(property, wireName)))))) .WithLeadingTrivia(EmissionSyntax.Documentation(model.Description ?? $"Represents a {DisplayName(model.Name)} value.")); if (model.BaseTypeName is not null) { @@ -123,56 +128,54 @@ private static AccessorListSyntax EmitAutoAccessors() => SyntaxFactory.AccessorL private static AccessorListSyntax EmitCollectionAccessors(TypeReferencePlan type, bool isRequired) { var copy = EmitCollectionCopy(type, SyntaxFactory.IdentifierName("value")); - StatementSyntax assignment; - var statements = new List(); + AccessorDeclarationSyntax initAccessor; if (type.IsNullable) { - assignment = SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - SyntaxFactory.IdentifierName("field"), - SyntaxFactory.ConditionalExpression( - SyntaxFactory.IsPatternExpression( - SyntaxFactory.IdentifierName("value"), - SyntaxFactory.ConstantPattern(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))), - SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression), - copy))); + initAccessor = EmitExpressionBodiedInit(SyntaxFactory.ConditionalExpression( + SyntaxFactory.IsPatternExpression( + SyntaxFactory.IdentifierName("value"), + SyntaxFactory.ConstantPattern(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))), + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression), + copy)); } else if (!isRequired) { - var empty = EmitCollectionInitializer(type) - ?? throw new InvalidOperationException("A non-null collection plan has no empty initializer."); - assignment = SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - SyntaxFactory.IdentifierName("field"), - SyntaxFactory.ConditionalExpression( - SyntaxFactory.IsPatternExpression( - SyntaxFactory.IdentifierName("value"), - SyntaxFactory.ConstantPattern(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))), - empty, - copy))) - .WithLeadingTrivia( - SyntaxFactory.Comment("// Source-generated deserialization passes null for an absent optional init-only collection."), - SyntaxFactory.EndOfLine("\n")); + initAccessor = EmitExpressionBodiedInit(EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("OptionalCollectionInput"), "Normalize"), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("value")), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("field")), + SyntaxFactory.Argument(Projection( + "input", + EmitCollectionCopy(type, SyntaxFactory.IdentifierName("input")))))); } else { + var statements = new List(); statements.AddRange(EmissionSyntax.ArgumentNullGuard("value")); - assignment = SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + statements.Add(SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("field"), - copy)); + copy))); + initAccessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.InitAccessorDeclaration) + .WithBody(SyntaxFactory.Block(statements)); } - statements.Add(assignment); return SyntaxFactory.AccessorList(SyntaxFactory.List( [ SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)), - SyntaxFactory.AccessorDeclaration(SyntaxKind.InitAccessorDeclaration) - .WithBody(SyntaxFactory.Block(statements)), + initAccessor, ])); } + private static AccessorDeclarationSyntax EmitExpressionBodiedInit(ExpressionSyntax value) => + SyntaxFactory.AccessorDeclaration(SyntaxKind.InitAccessorDeclaration) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("field"), + value))) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + private static ExpressionSyntax? EmitCollectionInitializer(TypeReferencePlan type) { if (!type.IsCollection || type.IsNullable) @@ -284,6 +287,7 @@ private static ObjectCreationExpressionSyntax EmitEmptyDictionary(DictionaryType SyntaxFactory.Literal(property.LiteralValue ?? throw new InvalidOperationException("String literal had no value."))), LiteralKind.Boolean when bool.TryParse(property.LiteralValue, out var value) => SyntaxFactory.LiteralExpression( value ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression), + LiteralKind.Number => throw new InvalidOperationException($"Property '{property.Name}' uses a number literal, which has no emission consumer."), _ => throw new InvalidOperationException($"Property '{property.Name}' has an invalid literal plan."), }; @@ -297,6 +301,11 @@ private static IReadOnlyList CollectUsings(ObjectModelPlan model) foreach (var property in model.Properties) { + if (property.Type is { IsCollection: true, IsNullable: false } && !property.IsRequired) + { + _ = result.Add("OpenCode.Sdk.Internal.Serialization"); + } + CollectTypeUsings(property.Type, result); } @@ -345,8 +354,21 @@ private static void CollectTypeUsings(TypeReferencePlan type, ISet using : throw new InvalidOperationException($"Model '{model.Name}' references absent union '{model.BaseTypeName}'."); } - private static bool IsDiscriminator(ModelPropertyPlan property, UnionPlan union) => - property.IsLiteral && string.Equals(property.WireName, union.MarkerWireName, StringComparison.Ordinal); + private static bool IsDiscriminator(ModelPropertyPlan property, string markerWireName) => + property.IsLiteral && string.Equals(property.WireName, markerWireName, StringComparison.Ordinal); + + private static List GetChainMarkerWireNames(UnionPlan? baseUnion, IReadOnlyDictionary unions) + { + var result = new List(); + var current = baseUnion; + while (current is not null) + { + result.Add(current.MarkerWireName); + current = current.BaseTypeName is not null && unions.TryGetValue(current.BaseTypeName, out var outer) ? outer : null; + } + + return result; + } private static string DisplayName(string name) => string.Join(' ', CSharpNamePolicy.SplitWords(name).Select(static word => word.ToLowerInvariant())); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/OperationMethodEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/OperationMethodEmitter.cs new file mode 100644 index 000000000..976cb45d5 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/OperationMethodEmitter.cs @@ -0,0 +1,122 @@ +using System.Globalization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using OpenCode.Sdk.Tools.Generator.Binding; +using OpenCode.Sdk.Tools.Generator.Binding.Models; + +namespace OpenCode.Sdk.Tools.Generator.Emission; + +/// Emits one virtual operation method delegating once into the pipeline. +internal static class OperationMethodEmitter +{ + public static MethodDeclarationSyntax Emit(OperationPlan operation) + { + ArgumentNullException.ThrowIfNull(operation); + + var methodParameters = operation.Parameters + .Where(static parameter => !parameter.IsHandleParameter) + .ToArray(); + var statements = new List(); + foreach (var parameter in methodParameters) + { + statements.AddRange(EmissionSyntax.ArgumentNullOrEmptyGuard(parameter.Name)); + } + + statements.Add(SyntaxFactory.ReturnStatement(EmitDelegation(operation))); + return SyntaxFactory.MethodDeclaration( + TypeSyntaxEmitter.Generic("Task", TypeSyntaxEmitter.EmitNamed(operation.Envelope.ResponseTypeName)), + operation.MethodName) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.VirtualKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(EmitParameters(methodParameters)))) + .WithBody(SyntaxFactory.Block(statements)) + .WithLeadingTrivia(EmitDocumentation(operation, methodParameters)); + } + + private static IEnumerable EmitParameters(IReadOnlyList methodParameters) + { + foreach (var parameter in methodParameters) + { + yield return SyntaxFactory.Parameter(SyntaxFactory.Identifier(parameter.Name)) + .WithType(TypeSyntaxEmitter.EmitNamed(parameter.TypeName)); + } + + yield return SyntaxFactory.Parameter(SyntaxFactory.Identifier("options")) + .WithType(SyntaxFactory.NullableType(TypeSyntaxEmitter.EmitNamed("OpenCodeRequestOptions"))) + .WithDefault(SyntaxFactory.EqualsValueClause(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))); + yield return SyntaxFactory.Parameter(SyntaxFactory.Identifier("cancellationToken")) + .WithType(TypeSyntaxEmitter.EmitNamed("CancellationToken")) + .WithDefault(SyntaxFactory.EqualsValueClause(SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression))); + } + + private static InvocationExpressionSyntax EmitDelegation(OperationPlan operation) => + EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("Pipeline"), "ExecuteAsync"), + SyntaxFactory.Argument(EmissionSyntax.MemberAccess( + SyntaxFactory.IdentifierName("HttpMethod"), + CSharpNamePolicy.ToPascalCase(operation.HttpMethod))), + SyntaxFactory.Argument(EmitRoute(operation)), + SyntaxFactory.Argument(EmissionSyntax.MemberAccess( + SyntaxFactory.IdentifierName(operation.Envelope.AdapterTypeName), + "Instance")), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("options")), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("cancellationToken"))); + + /// Route arguments come from the method for ordinary parameters and from the bound handle state otherwise. + private static ExpressionSyntax EmitRoute(OperationPlan operation) + { + var member = EmissionSyntax.MemberAccess( + EmissionSyntax.MemberAccess( + SyntaxFactory.IdentifierName("OpenCodeRoutes"), + operation.RouteContainerName), + operation.RouteMemberName); + if (operation.Parameters.Count is 0) + { + return member; + } + + return EmissionSyntax.Invocation(member, + [ + .. operation.Parameters.Select(static parameter => SyntaxFactory.Argument(parameter.IsHandleParameter + ? SyntaxFactory.IdentifierName(CSharpNamePolicy.ToPascalCase(parameter.Name)) + : SyntaxFactory.IdentifierName(parameter.Name))), + ]); + } + + private static SyntaxTriviaList EmitDocumentation(OperationPlan operation, IReadOnlyList methodParameters) + { + var parameters = new List(); + parameters.AddRange(methodParameters.Select(static parameter => + new DocumentedParameter(parameter.Name, $"The '{parameter.WireName}' route value."))); + parameters.Add(new DocumentedParameter("options", "The per-call options.")); + parameters.Add(new DocumentedParameter("cancellationToken", "The cancellation token.")); + + var exceptions = new List(); + if (operation.ErrorMap.Statuses.Count > 0) + { + var statuses = string.Join( + ", ", + operation.ErrorMap.Statuses.Select(static status => status.StatusCode.ToString(CultureInfo.InvariantCulture))); + exceptions.Add(new DocumentedException( + "OpenCodeApiException", + $"The API returned an error status (declared: {statuses}) and NoThrow was not selected.")); + } + else + { + exceptions.Add(new DocumentedException( + "OpenCodeApiException", + "The API returned an error status and NoThrow was not selected.")); + } + + exceptions.Add(new DocumentedException( + "OpenCodeTransportException", + "The server could not be reached or returned a malformed success body.")); + return EmissionSyntax.MemberDocumentation( + operation.Summary ?? operation.Description ?? $"Calls the '{operation.RouteTemplate}' operation.", + parameters, + $"The '{operation.Envelope.ResponseTypeName}' envelope.", + exceptions); + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/OptionalCollectionInputEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/OptionalCollectionInputEmitter.cs new file mode 100644 index 000000000..75cf3be71 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/OptionalCollectionInputEmitter.cs @@ -0,0 +1,50 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace OpenCode.Sdk.Tools.Generator.Emission; + +internal static class OptionalCollectionInputEmitter +{ + public static GeneratedSource Emit() + { + var typeParameter = SyntaxFactory.IdentifierName("T"); + var normalize = SyntaxFactory.MethodDeclaration(typeParameter, "Normalize") + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.InternalKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword))) + .WithTypeParameterList(SyntaxFactory.TypeParameterList( + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.TypeParameter("T")))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Parameter(SyntaxFactory.Identifier("value")) + .WithType(SyntaxFactory.NullableType(typeParameter)), + SyntaxFactory.Parameter(SyntaxFactory.Identifier("empty")) + .WithType(typeParameter), + SyntaxFactory.Parameter(SyntaxFactory.Identifier("copy")) + .WithType(TypeSyntaxEmitter.Generic("Func", typeParameter, typeParameter)), + ]))) + .WithConstraintClauses(SyntaxFactory.SingletonList( + SyntaxFactory.TypeParameterConstraintClause(typeParameter) + .WithConstraints(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.ClassOrStructConstraint(SyntaxKind.ClassConstraint))))) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ConditionalExpression( + SyntaxFactory.IsPatternExpression( + SyntaxFactory.IdentifierName("value"), + SyntaxFactory.ConstantPattern(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))), + SyntaxFactory.IdentifierName("empty"), + EmissionSyntax.Invocation( + SyntaxFactory.IdentifierName("copy"), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("value")))))) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + var declaration = SyntaxFactory.ClassDeclaration("OptionalCollectionInput") + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.InternalKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword))) + .WithMembers(SyntaxFactory.SingletonList(normalize)); + var unit = EmissionSyntax.CompilationUnit( + "OpenCode.Sdk.Internal.Serialization", + ["System"], + [declaration]); + return EmissionSyntax.CreateSource("Internal/Serialization/OptionalCollectionInput.cs", unit); + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs new file mode 100644 index 000000000..ca2729ac5 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs @@ -0,0 +1,172 @@ +using System.Globalization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using OpenCode.Sdk.Tools.Generator.Binding.Models; + +namespace OpenCode.Sdk.Tools.Generator.Emission; + +internal static class ResponseAdapterEmitter +{ + public static IReadOnlyList Emit(IReadOnlyList clients) + { + ArgumentNullException.ThrowIfNull(clients); + + return Array.AsReadOnly( + [ + .. clients + .SelectMany(static client => client.Operations) + .OrderBy(static operation => operation.Envelope.AdapterTypeName, StringComparer.Ordinal) + .Select(static operation => EmitAdapter(operation)), + ]); + } + + private static GeneratedSource EmitAdapter(OperationPlan operation) + { + var envelope = operation.Envelope; + var members = new List(); + members.AddRange(operation.ErrorMap.Statuses.Select(static status => EmitTagSet(status))); + members.Add(SyntaxFactory.ConstructorDeclaration(envelope.AdapterTypeName) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PrivateKeyword))) + .WithBody(SyntaxFactory.Block())); + members.Add(EmitInstance(envelope.AdapterTypeName)); + members.Add(EmitAdapt(operation)); + var declaration = SyntaxFactory.ClassDeclaration(envelope.AdapterTypeName) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.InternalKeyword), + SyntaxFactory.Token(SyntaxKind.SealedKeyword))) + .WithBaseList(SyntaxFactory.BaseList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.SimpleBaseType(TypeSyntaxEmitter.Generic( + "ResponseAdapter", + TypeSyntaxEmitter.EmitNamed(envelope.ResponseTypeName)))))) + .WithMembers(SyntaxFactory.List(members)) + .WithLeadingTrivia(EmissionSyntax.Documentation( + $"Adapts the '{operation.HttpMethod.ToUpperInvariant()} {operation.RouteTemplate}' responses onto '{envelope.ResponseTypeName}'.")); + var unit = EmissionSyntax.CompilationUnit( + "OpenCode.Sdk.Internal.ResponseAdapters", + ["System", "OpenCode.Sdk.Internal.Serialization"], + [declaration]); + return EmissionSyntax.CreateSource($"Internal/ResponseAdapters/{envelope.AdapterTypeName}.cs", unit); + } + + private static FieldDeclarationSyntax EmitTagSet(ErrorStatusPlan status) + { + var tags = status.Tags.Select(static tag => (ExpressionSyntax)SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(tag.Tag))); + return SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration( + SyntaxFactory.ArrayType( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), + SyntaxFactory.SingletonList(SyntaxFactory.ArrayRankSpecifier( + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.OmittedArraySizeExpression())))), + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.VariableDeclarator(TagSetName(status.StatusCode)) + .WithInitializer(SyntaxFactory.EqualsValueClause(SyntaxFactory.CollectionExpression( + SyntaxFactory.SeparatedList(tags.Select(SyntaxFactory.ExpressionElement) + .Cast()))))))) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PrivateKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword), + SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword))); + } + + private static PropertyDeclarationSyntax EmitInstance(string adapterTypeName) => + SyntaxFactory.PropertyDeclaration(TypeSyntaxEmitter.EmitNamed(adapterTypeName), "Instance") + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword))) + .WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.SingletonList( + SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))))) + .WithInitializer(SyntaxFactory.EqualsValueClause(SyntaxFactory.ObjectCreationExpression( + TypeSyntaxEmitter.EmitNamed(adapterTypeName)) + .WithArgumentList(SyntaxFactory.ArgumentList()))) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) + .WithLeadingTrivia(EmissionSyntax.Documentation("Gets the shared adapter instance.")); + + private static MethodDeclarationSyntax EmitAdapt(OperationPlan operation) + { + var envelope = operation.Envelope; + var arms = new List + { + SyntaxFactory.SwitchExpressionArm( + SyntaxFactory.ConstantPattern(Number(200)), + EmitSuccessCreation(envelope)), + // Any other 2xx is outside the declared contract: a protocol failure, never an API error. + SyntaxFactory.SwitchExpressionArm( + SyntaxFactory.BinaryPattern( + SyntaxKind.AndPattern, + SyntaxFactory.RelationalPattern(SyntaxFactory.Token(SyntaxKind.GreaterThanEqualsToken), Number(200)), + SyntaxFactory.RelationalPattern(SyntaxFactory.Token(SyntaxKind.LessThanToken), Number(300))), + SyntaxFactory.ThrowExpression(EmissionSyntax.Invocation( + SyntaxFactory.IdentifierName("UndeclaredSuccessFailure"), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("status"))))), + }; + arms.AddRange(operation.ErrorMap.Statuses.Select(status => SyntaxFactory.SwitchExpressionArm( + SyntaxFactory.ConstantPattern(Number(status.StatusCode)), + EmitErrorCreation(envelope, SyntaxFactory.IdentifierName(TagSetName(status.StatusCode)))))); + arms.Add(SyntaxFactory.SwitchExpressionArm( + SyntaxFactory.DiscardPattern(), + EmitErrorCreation(envelope, SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)))); + var statements = new List(); + statements.AddRange(EmissionSyntax.ArgumentNullGuard("rawBody")); + statements.Add(SyntaxFactory.ReturnStatement(SyntaxFactory.SwitchExpression( + SyntaxFactory.IdentifierName("status"), + SyntaxFactory.SeparatedList(arms)))); + return SyntaxFactory.MethodDeclaration(TypeSyntaxEmitter.EmitNamed(envelope.ResponseTypeName), "Adapt") + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.OverrideKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Parameter(SyntaxFactory.Identifier("status")) + .WithType(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword))), + SyntaxFactory.Parameter(SyntaxFactory.Identifier("rawBody")) + .WithType(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword))), + ]))) + .WithBody(SyntaxFactory.Block(statements)) + .WithLeadingTrivia(EmissionSyntax.Documentation("Maps one buffered response onto the typed envelope.")); + } + + private static ObjectCreationExpressionSyntax EmitSuccessCreation(EnvelopePlan envelope) + { + var reader = envelope.HasDataEnvelope ? "ReadDataPayload" : "ReadBarePayload"; + var payload = EmissionSyntax.Invocation( + SyntaxFactory.IdentifierName(reader), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("rawBody")), + SyntaxFactory.Argument(EmissionSyntax.MemberAccess( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("OpenCodeJsonContext"), "Default"), + envelope.PayloadTypeName))); + return SyntaxFactory.ObjectCreationExpression(TypeSyntaxEmitter.EmitNamed(envelope.ResponseTypeName)) + .WithInitializer(SyntaxFactory.InitializerExpression( + SyntaxKind.ObjectInitializerExpression, + SyntaxFactory.SeparatedList( + [ + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName("Status"), + SyntaxFactory.IdentifierName("status")), + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName(envelope.PayloadName), + payload), + ]))); + } + + private static ObjectCreationExpressionSyntax EmitErrorCreation(EnvelopePlan envelope, ExpressionSyntax allowedTags) => + SyntaxFactory.ObjectCreationExpression(TypeSyntaxEmitter.EmitNamed(envelope.ResponseTypeName)) + .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("status")), + SyntaxFactory.Argument(EmissionSyntax.Invocation( + SyntaxFactory.IdentifierName("ReadTolerantError"), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("rawBody")), + SyntaxFactory.Argument(allowedTags))), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("rawBody")), + ]))); + + private static LiteralExpressionSyntax Number(int value) => + SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(value)); + + private static string TagSetName(int statusCode) => + $"Status{statusCode.ToString(CultureInfo.InvariantCulture)}Tags"; +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/RoutesEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/RoutesEmitter.cs new file mode 100644 index 000000000..7184756a1 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/RoutesEmitter.cs @@ -0,0 +1,168 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using OpenCode.Sdk.Tools.Generator.Binding.Models; + +namespace OpenCode.Sdk.Tools.Generator.Emission; + +internal static class RoutesEmitter +{ + public static GeneratedSource Emit(IReadOnlyList clients) + { + ArgumentNullException.ThrowIfNull(clients); + + var operations = clients.SelectMany(static client => client.Operations).ToArray(); + var containers = operations + .GroupBy(static operation => operation.RouteContainerName, StringComparer.Ordinal) + .OrderBy(static container => container.Key, StringComparer.Ordinal) + .Select(EmitContainer) + .ToArray(); + var declaration = SyntaxFactory.ClassDeclaration("OpenCodeRoutes") + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword))) + .WithMembers(SyntaxFactory.List(containers)) + .WithLeadingTrivia(EmissionSyntax.Documentation("Defines the wire route of every generated operation.")); + var usings = operations.Any(static operation => operation.Parameters.Count > 0) + ? new[] { "System" } + : []; + var unit = EmissionSyntax.CompilationUnit("OpenCode.Sdk", usings, [declaration]); + return EmissionSyntax.CreateSource("OpenCodeRoutes.cs", unit); + } + + private static ClassDeclarationSyntax EmitContainer(IGrouping container) + { + var members = new List(); + foreach (var operation in container.OrderBy(static operation => operation.RouteMemberName, StringComparer.Ordinal)) + { + if (operation.Parameters.Count is 0) + { + members.Add(EmitConst( + operation.RouteMemberName, + operation.RouteTemplate, + $"The '{operation.HttpMethod.ToUpperInvariant()} {operation.RouteTemplate}' route.")); + continue; + } + + members.Add(EmitConst( + $"{operation.RouteMemberName}Template", + operation.RouteTemplate, + $"The '{operation.HttpMethod.ToUpperInvariant()} {operation.RouteTemplate}' route template.")); + members.Add(EmitBuilder(operation)); + } + + return SyntaxFactory.ClassDeclaration(container.Key) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword))) + .WithMembers(SyntaxFactory.List(members)) + .WithLeadingTrivia(EmissionSyntax.Documentation($"Defines the '{container.Key}' routes.")); + } + + private static FieldDeclarationSyntax EmitConst(string name, string value, string documentation) => + SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.VariableDeclarator(name) + .WithInitializer(SyntaxFactory.EqualsValueClause(SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(value))))))) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.ConstKeyword))) + .WithLeadingTrivia(EmissionSyntax.Documentation(documentation)); + + private static MethodDeclarationSyntax EmitBuilder(OperationPlan operation) + { + var statements = new List(); + foreach (var parameter in operation.Parameters) + { + statements.AddRange(EmitRouteValueGuard(parameter.Name)); + } + + statements.Add(SyntaxFactory.ReturnStatement(EmitConcatenation(operation))); + var documentation = EmissionSyntax.MemberDocumentation( + $"Builds the '{operation.RouteTemplate}' route.", + [.. operation.Parameters.Select(static parameter => + new DocumentedParameter(parameter.Name, $"The '{parameter.WireName}' route value."))], + "The escaped route."); + return SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), + operation.RouteMemberName) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(operation.Parameters.Select( + static parameter => SyntaxFactory.Parameter(SyntaxFactory.Identifier(parameter.Name)) + .WithType(TypeSyntaxEmitter.EmitNamed(parameter.TypeName)))))) + .WithBody(SyntaxFactory.Block(statements)) + .WithLeadingTrivia(documentation); + } + + /// + /// Guards one route value: null/empty/whitespace is refused, and so are the dot segments + /// Uri canonicalization would silently rewrite into a different request path. + /// + private static StatementSyntax[] EmitRouteValueGuard(string parameterName) + { + var parameter = SyntaxFactory.IdentifierName(parameterName); + var whitespaceGuard = SyntaxFactory.ExpressionStatement(EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("ArgumentException"), "ThrowIfNullOrWhiteSpace"), + SyntaxFactory.Argument(parameter))); + var dotSegmentGuard = SyntaxFactory.IfStatement( + SyntaxFactory.IsPatternExpression( + parameter, + SyntaxFactory.BinaryPattern( + SyntaxKind.OrPattern, + SyntaxFactory.ConstantPattern(StringLiteral(".")), + SyntaxFactory.ConstantPattern(StringLiteral("..")))), + SyntaxFactory.Block(SyntaxFactory.ThrowStatement(SyntaxFactory.ObjectCreationExpression( + SyntaxFactory.IdentifierName("ArgumentException")) + .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList( + [ + SyntaxFactory.Argument(StringLiteral("Route values must not be dot segments.")), + SyntaxFactory.Argument(EmissionSyntax.Invocation( + SyntaxFactory.IdentifierName("nameof"), + SyntaxFactory.Argument(parameter))), + ])))))); + return [whitespaceGuard, dotSegmentGuard]; + } + + /// Folds the template into literal + escaped-parameter concatenation, in template order. + private static ExpressionSyntax EmitConcatenation(OperationPlan operation) + { + var pieces = new List(); + var template = operation.RouteTemplate; + var position = 0; + foreach (var parameter in operation.Parameters) + { + var token = $"{{{parameter.WireName}}}"; + var start = template.IndexOf(token, position, StringComparison.Ordinal); + if (start < 0) + { + throw new InvalidOperationException( + $"Route template '{template}' does not contain parameter '{parameter.WireName}'."); + } + + if (start > position) + { + pieces.Add(StringLiteral(template[position..start])); + } + + pieces.Add(EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("Uri"), "EscapeDataString"), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName(parameter.Name)))); + position = start + token.Length; + } + + if (position < template.Length) + { + pieces.Add(StringLiteral(template[position..])); + } + + return pieces.Aggregate(static (left, right) => + SyntaxFactory.BinaryExpression(SyntaxKind.AddExpression, left, right)); + } + + private static LiteralExpressionSyntax StringLiteral(string value) => + SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(value)); +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs index dab9b9fd7..b2e1c8fea 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs @@ -11,9 +11,19 @@ public static IReadOnlyList Emit(EmitPlan plan) sources.AddRange(ModelEmitter.Emit(plan)); sources.AddRange(UnionEmitter.Emit(plan.Unions)); sources.AddRange(RegistryEmitter.Emit(plan.Registry)); + sources.Add(OptionalCollectionInputEmitter.Emit()); + if (plan.Clients.Count > 0) + { + sources.Add(RoutesEmitter.Emit(plan.Clients)); + sources.AddRange(EnvelopeEmitter.Emit(plan.Clients)); + sources.AddRange(ResponseAdapterEmitter.Emit(plan.Clients)); + sources.AddRange(ClientEmitter.Emit(plan.Clients)); + } + + var duplicate = sources + .GroupBy(static source => source.RelativePath, StringComparer.Ordinal) + .FirstOrDefault(static group => group.Skip(1).Any()); - var duplicate = sources.GroupBy(static source => source.RelativePath, StringComparer.Ordinal) - .FirstOrDefault(static group => group.Count() > 1); if (duplicate is not null) { throw new InvalidOperationException($"Multiple emitters produced '{duplicate.Key}'."); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/TypeSyntaxEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/TypeSyntaxEmitter.cs index f340b0b81..024e77de0 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/TypeSyntaxEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/TypeSyntaxEmitter.cs @@ -28,6 +28,7 @@ public static TypeSyntax Emit(TypeReferencePlan plan) { LiteralKind.String => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), LiteralKind.Boolean => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.BoolKeyword)), + LiteralKind.Number => throw new InvalidOperationException("Number markers have no emission consumer."), _ => throw new InvalidOperationException($"Unknown literal kind '{kind}'."), }; diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/UnionEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/UnionEmitter.cs index efaf6e019..4202d690e 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/UnionEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/UnionEmitter.cs @@ -25,7 +25,7 @@ public static IReadOnlyList Emit(IReadOnlyList union private static GeneratedSource EmitBase(UnionPlan union) { - var converterTypeName = string.Concat(union.Name, "JsonConverter"); + var converterTypeName = $"{union.Name}JsonConverter"; var marker = SyntaxFactory.PropertyDeclaration(TypeSyntaxEmitter.EmitMarker(union.MarkerKind), union.MarkerName) .WithModifiers(SyntaxFactory.TokenList( SyntaxFactory.Token(SyntaxKind.PublicKeyword), @@ -46,6 +46,12 @@ private static GeneratedSource EmitBase(UnionPlan union) SyntaxFactory.AttributeArgument(SyntaxFactory.TypeOfExpression(TypeSyntaxEmitter.EmitNamed(converterTypeName))))) .WithMembers(SyntaxFactory.SingletonList(marker)) .WithLeadingTrivia(EmissionSyntax.Documentation(union.Description ?? $"Represents a {DisplayName(union.Name)} union.")); + if (union.BaseTypeName is not null) + { + declaration = declaration.WithBaseList(SyntaxFactory.BaseList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.SimpleBaseType(TypeSyntaxEmitter.EmitNamed(union.BaseTypeName))))); + } + var unit = EmissionSyntax.CompilationUnit( union.Namespace, ["OpenCode.Sdk.Internal.Serialization", "System.Text.Json.Serialization"], @@ -56,13 +62,18 @@ private static GeneratedSource EmitBase(UnionPlan union) private static GeneratedSource EmitUnknown(UnionPlan union) { var markerType = TypeSyntaxEmitter.EmitMarker(union.MarkerKind); - var members = new MemberDeclarationSyntax[] + var members = new List { EmitUnknownMarkerField(markerType), EmitUnknownConstructor(union, markerType), EmitUnknownMarkerProperty(union, markerType), - EmitUnknownPayloadProperty(), }; + if (union.FixedMarker is { } fixedMarker) + { + members.Add(EmitFixedMarkerProperty(fixedMarker)); + } + + members.Add(EmitUnknownPayloadProperty()); var declaration = SyntaxFactory.RecordDeclaration(SyntaxFactory.Token(SyntaxKind.RecordKeyword), union.UnknownTypeName) .WithModifiers(SyntaxFactory.TokenList( SyntaxFactory.Token(SyntaxKind.PublicKeyword), @@ -121,10 +132,21 @@ private static PropertyDeclarationSyntax EmitUnknownMarkerProperty(UnionPlan uni .WithModifiers(SyntaxFactory.TokenList( SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.OverrideKeyword))) + .AddAttributeLists(EmissionSyntax.Attribute("JsonPropertyName", EmissionSyntax.StringArgument(union.MarkerWireName))) .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.IdentifierName("_marker"))) .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) .WithLeadingTrivia(EmissionSyntax.Documentation($"Gets the unrecognized '{union.MarkerWireName}' marker.")); + private static PropertyDeclarationSyntax EmitFixedMarkerProperty(UnionFixedMarkerPlan marker) => + SyntaxFactory.PropertyDeclaration(TypeSyntaxEmitter.EmitMarker(marker.Kind), marker.Name) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.OverrideKeyword))) + .AddAttributeLists(EmissionSyntax.Attribute("JsonPropertyName", EmissionSyntax.StringArgument(marker.WireName))) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(EmitMarkerLiteral(marker.Kind, marker.Value))) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) + .WithLeadingTrivia(EmissionSyntax.Documentation($"Gets the fixed '{marker.WireName}' marker of this nested union.")); + private static PropertyDeclarationSyntax EmitUnknownPayloadProperty() => SyntaxFactory.PropertyDeclaration(SyntaxFactory.IdentifierName("JsonElement"), "Payload") .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) @@ -135,7 +157,7 @@ private static PropertyDeclarationSyntax EmitUnknownPayloadProperty() => private static GeneratedSource EmitConverter(UnionPlan union) { - var converterName = string.Concat(union.Name, "JsonConverter"); + var converterName = $"{union.Name}JsonConverter"; var declaration = SyntaxFactory.ClassDeclaration(converterName) .WithModifiers(SyntaxFactory.TokenList( SyntaxFactory.Token(SyntaxKind.InternalKeyword), @@ -285,6 +307,7 @@ private static IfStatementSyntax EmitMarkerPresenceCheck(UnionPlan union) { LiteralKind.String => EmitStringMarkerRead(union), LiteralKind.Boolean => EmitBooleanMarkerRead(union), + LiteralKind.Number => throw new InvalidOperationException($"Union '{union.Name}' uses a number marker, which has no emission consumer."), _ => throw new InvalidOperationException($"Unknown marker kind '{union.MarkerKind}'."), }; @@ -298,11 +321,22 @@ private static IReadOnlyList EmitStringMarkerRead(UnionPlan uni ThrowJson($"The '{union.MarkerWireName}' marker must be a string.")), Local("marker", EmissionSyntax.Invocation( EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("markerElement"), "GetString"))), + // An empty or whitespace marker is malformed input, classified before any dispatch so the + // unknown-variant carrier's own guard can never surface as a BCL escape (ADR-0009). The + // explicit null disjunct carries the non-null flow fact on TFMs whose BCL lacks the + // IsNullOrWhiteSpace annotation. SyntaxFactory.IfStatement( - SyntaxFactory.IsPatternExpression( - SyntaxFactory.IdentifierName("marker"), - SyntaxFactory.ConstantPattern(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))), - ThrowJson($"The '{union.MarkerWireName}' marker cannot be null.")), + SyntaxFactory.BinaryExpression( + SyntaxKind.LogicalOrExpression, + SyntaxFactory.IsPatternExpression( + SyntaxFactory.IdentifierName("marker"), + SyntaxFactory.ConstantPattern(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))), + EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), + "IsNullOrWhiteSpace"), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("marker")))), + ThrowJson($"The '{union.MarkerWireName}' marker must be a non-empty string.")), ]; private static IReadOnlyList EmitBooleanMarkerRead(UnionPlan union) @@ -396,6 +430,32 @@ private static MethodDeclarationSyntax EmitWrite(UnionPlan union) statements.AddRange(EmissionSyntax.ArgumentNullGuard("value")); statements.AddRange(EmissionSyntax.ArgumentNullGuard("options")); statements.Add(SyntaxFactory.IfStatement(unknownPattern, writeUnknown)); + + // A nested-union variant must serialize through its declared base type so the nested + // union's converter runs (its unknown carrier writes the raw payload); runtime-type + // metadata would serialize the carrier as an ordinary record. + foreach (var typeName in union.Variants.Where(static variant => variant.IsNestedUnion).Select(static variant => variant.TypeName)) + { + var variableName = $"nested{typeName}"; + var nestedPattern = SyntaxFactory.IsPatternExpression( + SyntaxFactory.IdentifierName("value"), + SyntaxFactory.DeclarationPattern( + SyntaxFactory.IdentifierName(typeName), + SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier(variableName)))); + var nestedSerialize = EmissionSyntax.Invocation( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("JsonSerializer"), "Serialize"), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("writer")), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName(variableName)), + SyntaxFactory.Argument(EmissionSyntax.MemberAccess( + EmissionSyntax.MemberAccess(SyntaxFactory.IdentifierName("OpenCodeJsonContext"), "Default"), + typeName))); + statements.Add(SyntaxFactory.IfStatement( + nestedPattern, + SyntaxFactory.Block( + SyntaxFactory.ExpressionStatement(nestedSerialize), + SyntaxFactory.ReturnStatement()))); + } + statements.Add(Local("typeInfo", typeInfo)); statements.Add(SyntaxFactory.ExpressionStatement(serialize)); return SyntaxFactory.MethodDeclaration( @@ -434,6 +494,7 @@ private static ObjectCreationExpressionSyntax JsonException(string message) => SyntaxFactory.Literal(value)), LiteralKind.Boolean when bool.TryParse(value, out var boolean) => SyntaxFactory.LiteralExpression( boolean ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression), + LiteralKind.Number => throw new InvalidOperationException("Number marker literals have no emission consumer."), _ => throw new InvalidOperationException($"Marker value '{value}' is invalid for '{kind}'."), }; diff --git a/tools/OpenCode.Sdk.Tools/Generator/GenerationCoordinator.cs b/tools/OpenCode.Sdk.Tools/Generator/GenerationCoordinator.cs index a27c09e47..b7b0ec2ef 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/GenerationCoordinator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/GenerationCoordinator.cs @@ -4,7 +4,6 @@ using OpenCode.Sdk.Tools.Generator.Binding.Abstractions; using OpenCode.Sdk.Tools.Generator.Emission; using OpenCode.Sdk.Tools.Generator.Ingestion.Abstractions; -using OpenCode.Sdk.Tools.Generator.Ingestion.Models; using OpenCode.Sdk.Tools.Output.Abstractions; namespace OpenCode.Sdk.Tools.Generator; @@ -16,59 +15,59 @@ internal sealed class GenerationCoordinator( ISpecBinder binder, IGenerationWriter writer) { - private readonly ISpecBinder _binder = binder ?? throw new ArgumentNullException(nameof(binder)); - private readonly CurationLoader _curationLoader = curationLoader ?? throw new ArgumentNullException(nameof(curationLoader)); private readonly ISpecIngestion _ingestion = ingestion ?? throw new ArgumentNullException(nameof(ingestion)); private readonly OperationSelectionLoader _selectionLoader = selectionLoader ?? throw new ArgumentNullException(nameof(selectionLoader)); + private readonly CurationLoader _curationLoader = curationLoader ?? throw new ArgumentNullException(nameof(curationLoader)); + private readonly ISpecBinder _binder = binder ?? throw new ArgumentNullException(nameof(binder)); private readonly IGenerationWriter _writer = writer ?? throw new ArgumentNullException(nameof(writer)); public async Task GenerateAsync(GenerationRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); + var document = await _ingestion.IngestAsync(request.SpecPath, cancellationToken).ConfigureAwait(false); var selection = await _selectionLoader.LoadAsync(request.ProfilePath, cancellationToken).ConfigureAwait(false); var curation = await _curationLoader.LoadAsync(request.CurationPath, cancellationToken).ConfigureAwait(false); + var plan = _binder.Bind(document, selection, curation); - var modern = plan.PendingOperations - .Where(static operation => operation.Surface == SpecSurface.Modern) - .Select(static operation => operation.OperationId) - .Order(StringComparer.Ordinal) - .ToArray(); - var legacy = plan.PendingOperations - .Where(static operation => operation.Surface == SpecSurface.Legacy) + + var pending = plan + .PendingOperations .Select(static operation => operation.OperationId) .Order(StringComparer.Ordinal) .ToArray(); - var marker = modern.Length + legacy.Length > 0 ? CreatePartialMarker(plan.SelectedOperationIds, modern, legacy) : null; - var writeResult = await _writer.WriteAsync( - request.OutputRoot, - request.ProjectPath, - SourceEmitter.Emit(plan), - marker, - request.Verify, - cancellationToken).ConfigureAwait(false); + + var marker = pending.Length > 0 ? CreatePartialMarker(plan.SelectedOperationIds, pending) : null; + + var writeResult = await _writer + .WriteAsync( + request.OutputRoot, + request.ProjectPath, + SourceEmitter.Emit(plan), + marker, + request.Verify, + cancellationToken) + .ConfigureAwait(false); + return new GenerationReport { SelectedOperationIds = plan.SelectedOperationIds, - PendingModernOperationIds = modern, - PendingLegacyOperationIds = legacy, + PendingOperationIds = pending, WriteResult = writeResult, }; } - private static string CreatePartialMarker(IReadOnlyList selected, string[] modern, string[] legacy) + private static string CreatePartialMarker(IReadOnlyList selected, string[] pending) { var content = new StringBuilder() .AppendLine("Generation is incomplete; packages must not be published.") .Append("Selected operations: ") .AppendLine(selected.Count.ToString(CultureInfo.InvariantCulture)) - .Append("Pending modern operations: ") - .AppendLine(modern.Length.ToString(CultureInfo.InvariantCulture)) - .Append("Pending legacy operations: ") - .AppendLine(legacy.Length.ToString(CultureInfo.InvariantCulture)); + .Append("Pending operations: ") + .AppendLine(pending.Length.ToString(CultureInfo.InvariantCulture)); AppendOperations(content, "Selected", selected); - AppendOperations(content, "Pending modern", modern); - AppendOperations(content, "Pending legacy", legacy); + AppendOperations(content, "Pending", pending); + return content.ToString().ReplaceLineEndings("\n"); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/GenerationReport.cs b/tools/OpenCode.Sdk.Tools/Generator/GenerationReport.cs index 091a74f5a..fe0ca0c29 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/GenerationReport.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/GenerationReport.cs @@ -14,17 +14,7 @@ public required IReadOnlyList SelectedOperationIds } } = Array.AsReadOnly(Array.Empty()); - public required IReadOnlyList PendingModernOperationIds - { - get; - init - { - ArgumentNullException.ThrowIfNull(value); - field = Array.AsReadOnly([.. value]); - } - } = Array.AsReadOnly(Array.Empty()); - - public required IReadOnlyList PendingLegacyOperationIds + public required IReadOnlyList PendingOperationIds { get; init diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/LiteralKind.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/LiteralKind.cs index 35561f7a1..3fd26bb89 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/LiteralKind.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/LiteralKind.cs @@ -8,4 +8,7 @@ public enum LiteralKind /// A JSON boolean literal. Boolean = 1, + + /// A JSON number literal. + Number = 2, } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/LiteralNode.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/LiteralNode.cs index 921ebc1b0..69c2246d0 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/LiteralNode.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/LiteralNode.cs @@ -1,6 +1,6 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Models; -/// Represents a schema constrained to one string or boolean value. +/// Represents a schema constrained to one string, number, or boolean value. public sealed record LiteralNode : SchemaNode { /// Gets the JSON primitive kind carried by the literal. diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/SpecOperation.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/SpecOperation.cs index 36a979435..38ffcae2d 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/SpecOperation.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/SpecOperation.cs @@ -14,10 +14,7 @@ public required string OperationId } } - /// Gets the operation surface. - public required SpecSurface Surface { get; init; } - - /// Gets operation-identifier segments without the modern v2. marker. + /// Gets operation-identifier segments without the v2. protocol prefix. public required IReadOnlyList Segments { get; diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/SpecSurface.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/SpecSurface.cs deleted file mode 100644 index d522ecb87..000000000 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/SpecSurface.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace OpenCode.Sdk.Tools.Generator.Ingestion.Models; - -/// Identifies the upstream operation generation represented by an operation. -public enum SpecSurface -{ - /// The modern operation surface whose operation identifiers begin with v2.. - Modern = 0, - - /// The legacy operation surface. - Legacy = 1, -} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs index d673f77f9..1a50b67d7 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs @@ -8,6 +8,7 @@ internal sealed class GraphKeyBuilder public string Root(string wireNameOrOpId) { ArgumentException.ThrowIfNullOrWhiteSpace(wireNameOrOpId); + return wireNameOrOpId; } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs index 11098345f..7dc76f5eb 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs @@ -68,8 +68,13 @@ internal sealed class LiteralClassifier return ProjectStringEnum(schema, values[0], location, errors); } - return schema.Type is JsonSchemaType.Boolean - ? ProjectBooleanEnum(schema, values[0], location, errors) + if (schema.Type is JsonSchemaType.Boolean) + { + return ProjectBooleanEnum(schema, values[0], location, errors); + } + + return schema.Type is JsonSchemaType.Number or JsonSchemaType.Integer + ? ProjectNumberEnum(schema, values[0], location, errors) : RefuseUnsupportedEnum(location, errors); } @@ -109,9 +114,27 @@ internal sealed class LiteralClassifier return null; } + private static LiteralNode? ProjectNumberEnum(OpenApiSchema schema, JsonNode? value, string location, IngestionErrorCollector errors) + { + if (value is JsonValue jsonValue && jsonValue.GetValueKind() is System.Text.Json.JsonValueKind.Number) + { + return new LiteralNode + { + Description = schema.Description, + Format = schema.Format, + Kind = LiteralKind.Number, + Value = jsonValue.ToJsonString(), + Dialect = LiteralDialect.SingleValueEnum, + }; + } + + errors.Add(location, "single-value number enum must contain a number"); + return null; + } + private static LiteralNode? RefuseUnsupportedEnum(string location, IngestionErrorCollector errors) { - errors.Add(location, "single-value enums are supported only on string or boolean schemas"); + errors.Add(location, "single-value enums are supported only on string, number, or boolean schemas"); return null; } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationIdentity.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationIdentity.cs index b55ee7dbd..f39a258ff 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationIdentity.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationIdentity.cs @@ -1,5 +1,3 @@ -using OpenCode.Sdk.Tools.Generator.Ingestion.Models; - namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; -internal sealed record OperationIdentity(SpecSurface Surface, IReadOnlyList Segments, bool HasWildcardPath); +internal sealed record OperationIdentity(IReadOnlyList Segments, bool HasWildcardPath); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationIdentityParser.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationIdentityParser.cs index e2fab2630..3b46f76e4 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationIdentityParser.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationIdentityParser.cs @@ -1,10 +1,8 @@ -using OpenCode.Sdk.Tools.Generator.Ingestion.Models; - namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; internal sealed class OperationIdentityParser { - private readonly string _modernPrefix = "v2."; + private readonly string _protocolPrefix = "v2."; public OperationIdentity? Parse(string operationId, string path, string location, IngestionErrorCollector errors) { @@ -13,9 +11,13 @@ internal sealed class OperationIdentityParser ArgumentException.ThrowIfNullOrWhiteSpace(location); ArgumentNullException.ThrowIfNull(errors); - var surface = operationId.StartsWith(_modernPrefix, StringComparison.Ordinal) ? SpecSurface.Modern : SpecSurface.Legacy; - var segmentSource = surface is SpecSurface.Modern ? operationId[_modernPrefix.Length..] : operationId; - var segments = segmentSource.Split('.'); + if (!operationId.StartsWith(_protocolPrefix, StringComparison.Ordinal)) + { + errors.Add(location, $"operationId '{operationId}' does not carry the '{_protocolPrefix}' protocol prefix"); + return null; + } + + var segments = operationId[_protocolPrefix.Length..].Split('.'); if (segments.Any(string.IsNullOrWhiteSpace)) { errors.Add(location, $"operationId '{operationId}' contains an empty segment"); @@ -27,7 +29,7 @@ internal sealed class OperationIdentityParser if (!hasWildcard || (wildcardIndex == path.Length - 1 && path.EndsWith("/*", StringComparison.Ordinal) && path.LastIndexOf('*', StringComparison.Ordinal) == wildcardIndex)) { - return new OperationIdentity(surface, Array.AsReadOnly(segments), hasWildcard); + return new OperationIdentity(Array.AsReadOnly(segments), hasWildcard); } errors.Add(location, $"path '{path}' has a wildcard outside the trailing '/*' position"); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs index f096645e8..c4dc24048 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs @@ -116,7 +116,6 @@ private void ProjectOperation(string path, KeyValuePair(out var reference) + || schema.Items is not OpenApiSchemaReference itemsReference + || !string.Equals(reference, "#/components/schemas/" + itemsReference.Reference.Id, StringComparison.Ordinal)) + { + state.Errors.Add(location, "items with prefixItems is supported only as a single prefix element sharing the items $ref target"); + return null; + } + + var item = _schemaProjector.Project(schema.Items, root, _keys.Append(pointer, "items"), state); + return item is null + ? null + : new ArrayNode + { + Description = schema.Description, + Format = schema.Format, + Item = item, + }; + } + private bool ValidateArity(OpenApiSchema schema, int arity, string root, string pointer, IngestionErrorCollector errors) { var isValid = true; diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs index 540efb8fb..5aa5bf5e9 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs @@ -76,7 +76,7 @@ public SchemaProjector(GraphKeyBuilder keys) (CoreSchemaShape.Primitive, JsonSchemaType.Number) => CreatePrimitive(schema, PrimitiveKind.Number), (CoreSchemaShape.Primitive, JsonSchemaType.Integer) => CreatePrimitive(schema, PrimitiveKind.Integer), (CoreSchemaShape.Primitive, JsonSchemaType.Boolean) => CreatePrimitive(schema, PrimitiveKind.Boolean), - (CoreSchemaShape.Array, _) => ProjectArray(schema, root, pointer, location, state), + (CoreSchemaShape.Array, _) => ProjectArray(schema, root, pointer, state), (CoreSchemaShape.Object, _) => _objectProjector.Project(schema, root, pointer, location, state), (CoreSchemaShape.Enum, _) => _enumProjector.Project(schema, location, state.Errors), (CoreSchemaShape.Literal, _) => _literalClassifier.Project(schema, location, state.Errors), @@ -89,12 +89,19 @@ public SchemaProjector(GraphKeyBuilder keys) }; } - private ArrayNode? ProjectArray(OpenApiSchema schema, string root, string pointer, string location, ProjectionState state) + private ArrayNode? ProjectArray(OpenApiSchema schema, string root, string pointer, ProjectionState state) { + // A bare {"type":"array"} constrains its elements to nothing: JSON Schema semantics + // admit any value, so the item is the explicit any-value node (the unrestricted-{} + // rule); mapping it to anything narrower would silently narrow the wire. if (schema.Items is null) { - state.Errors.Add(location, "array schema must declare items"); - return null; + return new ArrayNode + { + Description = schema.Description, + Format = schema.Format, + Item = new UnrestrictedNode(), + }; } var item = Project(schema.Items, root, _keys.Append(pointer, "items"), state); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaShapeClassifier.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaShapeClassifier.cs index aea840cd0..e468dbb35 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaShapeClassifier.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaShapeClassifier.cs @@ -32,6 +32,17 @@ public CoreSchemaShape Classify(OpenApiSchema schema, string location, Ingestion var populatedRules = _constraintRules.Where(rule => rule.IsPopulated(schema)).ToArray(); var constraints = populatedRules.Aggregate(Constraint.None, static (current, rule) => current | rule.Constraint); + + // The v2 dialect wraps validation keywords in a single-element allOf + // ({"type":"integer","allOf":[{"exclusiveMinimum":0}]}). A wrapper carrying only the + // validation/annotation vocabulary the projection deliberately ignores unwraps to the + // host's own shape; a wrapper with anything structural, consumed (format), referencing, + // applicator-typed, or unrecognized keeps the fail-closed refusal below. + if (constraints is (Constraint.Type | Constraint.AllOf) && IsValidationOnlyAllOfWrapper(schema)) + { + constraints = Constraint.Type; + } + var shape = (constraints, schema.Type) switch { (Constraint.None, _) => CoreSchemaShape.Unrestricted, @@ -40,11 +51,14 @@ public CoreSchemaShape Classify(OpenApiSchema schema, string location, Ingestion (Constraint.Type | Constraint.Enum, JsonSchemaType.String) when schema.Enum?.Count is 1 => CoreSchemaShape.Literal, (Constraint.Type | Constraint.Enum, JsonSchemaType.String) => CoreSchemaShape.Enum, (Constraint.Type | Constraint.Enum, JsonSchemaType.Boolean) => CoreSchemaShape.Literal, + (Constraint.Type | Constraint.Enum, JsonSchemaType.Number or JsonSchemaType.Integer) when schema.Enum?.Count is 1 => CoreSchemaShape + .Literal, (Constraint.Type | Constraint.Const, _) => CoreSchemaShape.Literal, (Constraint.AnyOf, _) => CoreSchemaShape.Union, (Constraint.OneOf, _) => CoreSchemaShape.Union, (Constraint.Type, JsonSchemaType.Null) => CoreSchemaShape.Null, (Constraint.Type | Constraint.PrefixItems, JsonSchemaType.Array) => CoreSchemaShape.Tuple, + (Constraint.Type | Constraint.Items | Constraint.PrefixItems, JsonSchemaType.Array) => CoreSchemaShape.Tuple, (Constraint.Type | Constraint.ContentSchema, JsonSchemaType.String) => CoreSchemaShape.JsonString, (Constraint.Type | Constraint.ContentMediaType | Constraint.ContentSchema, JsonSchemaType.String) => CoreSchemaShape.JsonString, (Constraint.Type, JsonSchemaType.Array) => CoreSchemaShape.Array, @@ -64,6 +78,45 @@ _ when IsObjectShape(constraints, schema.Type) => CoreSchemaShape.Object, return shape; } + private bool IsValidationOnlyAllOfWrapper(OpenApiSchema schema) + { + if (schema.AllOf is not { Count: 1 } || schema.AllOf[0] is not OpenApiSchema element) + { + return false; + } + + if (_constraintRules.Any(rule => rule.IsPopulated(element))) + { + return false; + } + + return element is + { + Format: null, + Not: null, + If: null, + Then: null, + Else: null, + Contains: null, + PropertyNames: null, + Discriminator: null, + Xml: null, + ExternalDocs: null, + Default: null, + Example: null, + Deprecated: false, + ReadOnly: false, + WriteOnly: false, + } + && element.DependentSchemas is not { Count: > 0 } + && element.DependentRequired is not { Count: > 0 } + && element.Definitions is not { Count: > 0 } + && element.Examples is not { Count: > 0 } + && element.Vocabulary is not { Count: > 0 } + && element.Extensions is not { Count: > 0 } + && element.UnrecognizedKeywords is not { Count: > 0 }; + } + private static bool IsObjectShape(Constraint constraints, JsonSchemaType? type) { const Constraint objectConstraints = Constraint.Type | Constraint.Required | Constraint.Properties | Constraint.PatternProperties diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs index 61f35e9c9..3b4673489 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs @@ -119,6 +119,21 @@ private bool ValidateSpecialNumberBranches(IList branches, strin var branchPointer = _keys.UnionBranch(pointer, keyword, index, marker: null); SchemaWallPolicy.Check(branch, string.Concat(root, branchPointer), state.Errors); + if (branch.AnyOf is not { Count: > 0 } nested) + { + continue; + } + + for (var innerIndex = 0; innerIndex < nested.Count; innerIndex++) + { + if (nested[innerIndex] is not OpenApiSchema inner) + { + continue; + } + + var innerPointer = _keys.UnionBranch(branchPointer, "anyOf", innerIndex, marker: null); + SchemaWallPolicy.Check(inner, string.Concat(root, innerPointer), state.Errors); + } } return state.Errors.Count == errorCount; @@ -133,6 +148,7 @@ private bool ValidateSpecialNumberBranches(IList branches, strin { var source = sourceBranches[index]; var marker = _literalClassifier.FindFirstMarker(source); + var isMarkedUnionReference = marker is null && IsMarkedUnionReference(source); var branchPointer = _keys.UnionBranch(pointer, keyword, index, marker); var branchLocation = string.Concat(root, branchPointer); if (source is OpenApiSchema { Type: JsonSchemaType.Null } concrete) @@ -142,7 +158,7 @@ private bool ValidateSpecialNumberBranches(IList branches, strin var shape = _shapeClassifier.Classify(concrete, branchLocation, state.Errors); if (state.Errors.Count == errorCount && shape is CoreSchemaShape.Null) { - branches.Add(new ProjectedBranch(Node: null, marker, IsNull: true)); + branches.Add(new ProjectedBranch(Node: null, marker, IsNull: true, IsMarkedUnionReference: false)); } else { @@ -159,12 +175,23 @@ private bool ValidateSpecialNumberBranches(IList branches, strin continue; } - branches.Add(new ProjectedBranch(projected, marker, IsNull: false)); + branches.Add(new ProjectedBranch(projected, marker, IsNull: false, isMarkedUnionReference)); } return failed ? null : [.. branches]; } + /// + /// Accepts a branch referencing a nested marked union (exactly one level): every variant + /// of the referenced union is itself a marked object. The Binder validates that the + /// nested variants contribute one uniform outer tag; this records only markedness. + /// + private bool IsMarkedUnionReference(IOpenApiSchema source) => + source is OpenApiSchemaReference { Target: OpenApiSchema target } + && IsPureAnyOfBranch(target) + && target.AnyOf is { Count: > 1 } nested + && nested.All(branch => _literalClassifier.FindFirstMarker(branch) is not null); + private static ProjectedBranch[] DeduplicateReferences(IReadOnlyList branches) { var targets = new HashSet(StringComparer.Ordinal); @@ -181,7 +208,7 @@ private static SchemaNode CreateInner(IReadOnlyList branches, U return branches[0].Node ?? throw new InvalidOperationException("A non-null union branch had no projected node."); } - var classification = branches.All(static branch => branch.Marker is not null) + var classification = branches.All(static branch => branch.Marker is not null || branch.IsMarkedUnionReference) ? UnionClassification.Marked : UnionClassification.Structural; return new UnionNode @@ -200,6 +227,14 @@ private static bool IsSpecialNumber(IEnumerable branches) { var numberBranches = 0; var specialBranches = 0; + return CountSpecialNumberBranches(branches, allowNested: true, ref numberBranches, ref specialBranches) + && numberBranches is 1 + && specialBranches > 0; + } + + private static bool CountSpecialNumberBranches(IEnumerable branches, bool allowNested, ref int numberBranches, + ref int specialBranches) + { foreach (var branch in branches) { if (branch is not OpenApiSchema schema) @@ -213,6 +248,19 @@ private static bool IsSpecialNumber(IEnumerable branches) continue; } + // The v2 dialect nests the special-number union one level deeper + // (anyOf: [anyOf of number-and-specials, a redundant specials enum]); exactly one + // nesting level flattens, anything deeper or impure stays refused. + if (allowNested && IsPureAnyOfBranch(schema)) + { + if (!CountSpecialNumberBranches(schema.AnyOf!, allowNested: false, ref numberBranches, ref specialBranches)) + { + return false; + } + + continue; + } + if (!IsSpecialStringBranch(schema)) { return false; @@ -221,9 +269,18 @@ private static bool IsSpecialNumber(IEnumerable branches) specialBranches++; } - return numberBranches is 1 && specialBranches > 0; + return true; } + private static bool IsPureAnyOfBranch(OpenApiSchema schema) => + schema is { AnyOf.Count: > 0, Type: null, Enum: null, Const: null, Items: null, Properties: null, AdditionalProperties: null } + && schema.OneOf is not { Count: > 0 } + && schema.AllOf is not { Count: > 0 } + && schema.Required is not { Count: > 0 } + && schema.PatternProperties is not { Count: > 0 } + && schema.AdditionalPropertiesAllowed + && schema is { ContentEncoding: null, ContentMediaType: null, ContentSchema: null }; + private static bool IsSpecialStringBranch(OpenApiSchema schema) { if (schema.Type is not JsonSchemaType.String || HasCompetingNodeConstraints(schema)) @@ -260,5 +317,5 @@ private static bool HasCompetingNodeConstraints(OpenApiSchema schema) => || schema.ContentMediaType is not null || schema.ContentSchema is not null; - private sealed record ProjectedBranch(SchemaNode? Node, LiteralMarker? Marker, bool IsNull); + private sealed record ProjectedBranch(SchemaNode? Node, LiteralMarker? Marker, bool IsNull, bool IsMarkedUnionReference); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs index 4160797d8..07258d2c5 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs @@ -28,7 +28,9 @@ public async Task IngestAsync(string specPath, CancellationToken c var keys = new GraphKeyBuilder(); var schemaProjector = new SchemaProjector(keys); var state = new ProjectionState(errors, loaded.Document); - var operations = new OperationProjector(schemaProjector, keys).Project(loaded, state); + var operationProjector = new OperationProjector(schemaProjector, keys); + + var operations = operationProjector.Project(loaded, state); if (loaded.Document.Components?.Schemas is { } namedSchemas) { foreach (var (name, schema) in namedSchemas) @@ -53,7 +55,8 @@ private static void CheckDanglingReferences(IReadOnlyList operati IngestionErrorCollector errors) { var roots = operations - .SelectMany(static operation => operation.Parameters.Select(static parameter => parameter.Schema) + .SelectMany(static operation => operation + .Parameters.Select(static parameter => parameter.Schema) .Concat(operation.Responses.Where(static response => response.Schema is not null).Select(static response => response.Schema!)) .Append(operation.RequestBody?.Schema)) .Concat(schemas.Values) diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Walls/SchemaWallPolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Walls/SchemaWallPolicy.cs index 6fde243d3..b9db45a53 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Walls/SchemaWallPolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Walls/SchemaWallPolicy.cs @@ -4,8 +4,6 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Walls; internal static class SchemaWallPolicy { - private const string PrefixItemsAdmissionLocation = "Config/properties/plugin/items/anyOf/1"; - public static void Check(OpenApiSchema schema, string location, IngestionErrorCollector errors) { ArgumentNullException.ThrowIfNull(schema); @@ -20,21 +18,19 @@ public static void Check(OpenApiSchema schema, string location, IngestionErrorCo // Unrecognized keywords are the one construct nothing downstream can ever see: the // reader retains them raw and every typed consumer is blind to them. Silence here // would be silent wire loss, so they refuse — unlike typed annotation/validation - // members, which are known vocabulary and are deliberately ignored. + // members, which are known vocabulary and are deliberately ignored. The single + // exception is prefixItems: the classifier routes it and the adapter admits exactly + // two shapes (closed tuple, non-empty homogeneous array), refusing the rest located. if (schema.UnrecognizedKeywords is null) { return; } foreach (var keyword in schema - .UnrecognizedKeywords.Keys.Where(keyword => !IsAdmittedPrefixItems(keyword, location)) + .UnrecognizedKeywords.Keys.Where(static keyword => !string.Equals(keyword, "prefixItems", StringComparison.Ordinal)) .Order(StringComparer.Ordinal)) { errors.Add(location, $"unrecognized schema keyword '{keyword}' is not supported"); } } - - private static bool IsAdmittedPrefixItems(string keyword, string location) => - string.Equals(keyword, "prefixItems", StringComparison.Ordinal) - && string.Equals(location, PrefixItemsAdmissionLocation, StringComparison.Ordinal); } diff --git a/tools/OpenCode.Sdk.Tools/Output/GenerationWriter.cs b/tools/OpenCode.Sdk.Tools/Output/GenerationWriter.cs index 99d36b7c4..f7eeee55d 100644 --- a/tools/OpenCode.Sdk.Tools/Output/GenerationWriter.cs +++ b/tools/OpenCode.Sdk.Tools/Output/GenerationWriter.cs @@ -36,6 +36,8 @@ public async Task WriteAsync( var oldManifest = await LoadManifestAsync(outputRoot, cancellationToken).ConfigureAwait(false); var oldPaths = ValidateManifest(outputRoot, oldManifest); RefuseCaseOnlyPathChanges(oldPaths, newSources.Keys); + RefuseUnmanifestedOverwrites(outputRoot, oldPaths, newSources.Keys); + await RequireProvenanceOnManifestEntriesAsync(outputRoot, oldPaths, cancellationToken).ConfigureAwait(false); var trackedPaths = oldPaths .Concat(newSources.Keys) .Append(ManifestRelativePath) @@ -69,6 +71,12 @@ private Dictionary ValidateSources(string outputRoot, I ValidateRelativePath(outputRoot, source.RelativePath); ValidateGeneratedSourcePath(source.RelativePath); + // A headerless source would deadlock the next run's provenance requirement. + if (!GenerationProvenance.HasHeader(Encoding.UTF8.GetString(source.Utf8Source.Span))) + { + throw new InvalidOperationException($"Generated source '{source.RelativePath}' lacks the provenance header."); + } + if (!result.TryAdd(source.RelativePath, source)) { throw new InvalidOperationException($"Generated source path '{source.RelativePath}' is duplicated."); @@ -161,7 +169,7 @@ private async Task WriteManifestAsync(string outputRoot, IEnumerable pat { Files = [.. paths.Order(StringComparer.Ordinal)], }; - var json = string.Concat(JsonSerializer.Serialize(manifest, ToolJsonContext.Default.GenerationManifest), "\n"); + var json = $"{JsonSerializer.Serialize(manifest, ToolJsonContext.Default.GenerationManifest)}\n"; await WriteBytesAsync( ResolveOwnedPath(outputRoot, ManifestRelativePath), Encoding.UTF8.GetBytes(json), @@ -205,13 +213,49 @@ private void ValidateRelativePath(string outputRoot, string relativePath) private static void ValidateGeneratedSourcePath(string relativePath) { if (!relativePath.EndsWith(".cs", StringComparison.Ordinal) - || !(relativePath.StartsWith("Models/", StringComparison.Ordinal) - || relativePath.StartsWith("Internal/Serialization/", StringComparison.Ordinal))) + || !(!relativePath.Contains('/', StringComparison.Ordinal) + || relativePath.StartsWith("Models/", StringComparison.Ordinal) + || relativePath.StartsWith("Internal/Serialization/", StringComparison.Ordinal) + || relativePath.StartsWith("Internal/ResponseAdapters/", StringComparison.Ordinal))) { throw new InvalidOperationException($"Generated source path '{relativePath}' is outside the owned source directories."); } } + /// A new path that already exists on disk belongs to somebody else until the manifest owns it. + private void RefuseUnmanifestedOverwrites(string outputRoot, HashSet oldPaths, IEnumerable newPaths) + { + var overwrite = newPaths + .Where(path => !oldPaths.Contains(path)) + .Order(StringComparer.Ordinal) + .FirstOrDefault(path => _fileSystem.File.Exists(ResolveOwnedPath(outputRoot, path))); + if (overwrite is not null) + { + throw new InvalidOperationException($"Generated source path '{overwrite}' would overwrite an unmanifested file."); + } + } + + /// The writer only overwrites or deletes files that still carry the generated provenance header. + private async Task RequireProvenanceOnManifestEntriesAsync(string outputRoot, IReadOnlySet oldPaths, + CancellationToken cancellationToken) + { + foreach (var relativePath in oldPaths.Order(StringComparer.Ordinal)) + { + var path = ResolveOwnedPath(outputRoot, relativePath); + if (!_fileSystem.File.Exists(path)) + { + continue; + } + + var content = await _fileSystem.File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false); + if (!GenerationProvenance.HasHeader(content)) + { + throw new InvalidOperationException( + $"Manifest entry '{relativePath}' does not carry the provenance header; refusing to overwrite or delete it."); + } + } + } + private string ResolveOwnedPath(string outputRoot, string relativePath) { if (string.IsNullOrWhiteSpace(relativePath) diff --git a/tools/OpenCode.Sdk.Tools/ToolApp.cs b/tools/OpenCode.Sdk.Tools/ToolApp.cs index e2564c6b0..d7a336b5e 100644 --- a/tools/OpenCode.Sdk.Tools/ToolApp.cs +++ b/tools/OpenCode.Sdk.Tools/ToolApp.cs @@ -38,6 +38,7 @@ public static ServiceCollection CreateServices(Action? overr services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/tools/curation.json b/tools/curation.json index 377bf3011..29192073d 100644 --- a/tools/curation.json +++ b/tools/curation.json @@ -6,21 +6,20 @@ "session": { "placement": "client", "clientName": "Sessions", - "handleName": "SessionClient" + "handleName": "SessionClient", + "handleParameter": "sessionID" } }, - "envelopePayloadNames": { - "v2.session.message": "Message" - }, + "envelopePayloadNames": {}, "propertyOverrides": [ { - "schema": "PromptFileAttachment", + "schema": "Prompt.FileSource#/anyOf/type=uri", "property": "uri", "type": "uri", "reason": "The wire field carries a URI, but the pinned schema omits format: uri." }, { - "schema": "ToolFileContent", + "schema": "Tool.FileContent", "property": "uri", "type": "uri", "reason": "The wire field carries a URI, but the pinned schema omits format: uri."