From f35f7413f8b08a28925668c3d26c1dde9a308ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Wed, 12 Aug 2026 17:36:07 +0300 Subject: [PATCH 01/28] docs(research): inventory Polyfill capabilities --- .../14-polyfill-capability-inventory.md | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 docs/research/14-polyfill-capability-inventory.md diff --git a/docs/research/14-polyfill-capability-inventory.md b/docs/research/14-polyfill-capability-inventory.md new file mode 100644 index 00000000..4cd8a4dc --- /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` | Unset in product; `true` in the tooling project | Product uses ordinary internal types; tooling hides them from friend assemblies | +| `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). From 7f317f02163f6bcadde1d60554d5f620602d999c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 09:27:51 +0300 Subject: [PATCH 02/28] docs(plan): define M1 callable client arc --- docs/ROADMAP.md | 6 +- docs/adr/0004-generated-model-principles.md | 18 +- docs/adr/0007-error-model.md | 14 +- docs/adr/0008-generated-operation-methods.md | 9 +- docs/research/00-research-log.md | 42 +++- docs/research/11-error-channel-scope.md | 12 +- .../plans/2026-08-11-m1-walking-skeleton.md | 188 ------------------ .../2026-08-13-m1-arc-b-callable-client.md | 157 +++++++++++++++ .../2026-08-09-generator-architecture.md | 166 ++++++---------- .../specs/2026-08-09-public-api-design.md | 28 ++- .../2026-08-10-testing-architecture-design.md | 11 +- ...8-11-production-walking-skeleton-design.md | 18 +- 12 files changed, 322 insertions(+), 347 deletions(-) delete mode 100644 docs/superpowers/plans/2026-08-11-m1-walking-skeleton.md create mode 100644 docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9f5b0403..764e4e36 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # 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/`. @@ -12,7 +12,9 @@ SpecIR ingestion are in place. M1's compiler arc selects the two walking-skeleto 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`. +The active work is M1 Arc B's minimal transport and callable client with typed errors and +`NoThrow`; see the just-in-time +[M1 Arc B callable-client plan](superpowers/plans/2026-08-13-m1-arc-b-callable-client.md). ## Milestones diff --git a/docs/adr/0004-generated-model-principles.md b/docs/adr/0004-generated-model-principles.md index 037a827b..265cf0c6 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/0007-error-model.md b/docs/adr/0007-error-model.md index e9b9129a..61455def 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 ca5dcb24..e45deb7f 100644 --- a/docs/adr/0008-generated-operation-methods.md +++ b/docs/adr/0008-generated-operation-methods.md @@ -1,6 +1,6 @@ # 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 as a one-line delegation into the hand-written behavior core; behavior (retry, error @@ -27,5 +27,8 @@ 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 modern group curation: a row declares a client name, handle name, + and required path parameter. The Binder applies one mechanical rule to that declaration: + modern operations carrying the parameter emit on the handle with it partially applied; + collection operations stay on the collection client. The legacy surface remains flat. + Emitters never branch on operation IDs, wire group names, or concrete client names. diff --git a/docs/research/00-research-log.md b/docs/research/00-research-log.md index 0938506a..0bf54d9f 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,43 @@ 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. diff --git a/docs/research/11-error-channel-scope.md b/docs/research/11-error-channel-scope.md index 7bed62ee..fa34a208 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/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 5245c6ce..00000000 --- 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 00000000..005d08e5 --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md @@ -0,0 +1,157 @@ +# 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 modern `session` 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. Legacy operations remain flat and pending in M1. +- 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 1: Model Policy And Operation Binding + +**Areas:** `Generator/Binding/`, `ModelEmitter`, `tools/curation.json`, Binder/emitter tests, +and committed generated models. + +- [ ] 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. +- [ ] 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. +- [ ] Add `handleParameter` to modern group curation. Require it and `handleName` together, + validate it names a required path parameter, and never consume a query/body value. Keep all + pending legacy operations flat. +- [ ] Add consumed-only `ClientPlan`, `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, + and `ErrorMapPlan` plus an `OperationPlanBinder`. Reuse schema-resolved type names instead of + reinterpreting schemas. +- [ ] Bind only modern ordinary GET + JSON + path-parameter operations with one 200 success and + no body/query/stream/wildcard. Bind `{healthy}` to `HealthResponse.Healthy`, curated `{data}` + to `SessionMessageResponse.Message`, and the normalized 404 union to two semantic variants. +- [ ] 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; a legacy operation carrying `sessionID` must remain + flat. + +## 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. + +- [ ] 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. +- [ ] Implement endpoint validation/joining and owned-versus-injected `HttpClient` lifetime. + Disposing the root makes its wrappers unusable and never disposes a BYO client. +- [ ] Resolve explicit password before one-time `OPENCODE_SERVER_PASSWORD` fallback. Decorate + each request, never default headers, with Basic user `opencode`, per-call-over-client + `x-opencode-directory`, and `OpenCode.Sdk/` User-Agent. +- [ ] Use only `HttpClient.SendAsync(request, ResponseHeadersRead, cancellationToken)`. Own each + request and buffered response, preserve cancellation, and wrap network/protocol failures as + `OpenCodeTransportException`. +- [ ] 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. + +- [ ] 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. +- [ ] 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. +- [ ] 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. +- [ ] Add one micro-snapshot per emitter family, aggregate generated-source compilation, and + writer tests for mixed-root preservation, stale deletion, provenance, formatting, and drift. +- [ ] 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 + +- [ ] 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. +- [ ] Assert an undeclared status and a known tag returned at the wrong declared status both + produce `UnknownOpenCodeError` with the exact tag and raw payload. +- [ ] 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. +- [ ] 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. +- [ ] Run `generate`, clean `generate --verify`, Release build, supported test legs, format, + Slopwatch, and intentional pack refusal. +- [ ] Start a password-enabled real `opencode serve` in a directory containing a message. Obtain + IDs with authenticated raw calls to `GET /api/session?limit=1` and + `GET /api/session/{sessionID}/message?limit=1`, then call only the two generated operations + from a temporary `.scratchpad/` console. Print `Healthy`, 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 c3456a9d..ce885c8a 100644 --- a/docs/superpowers/specs/2026-08-09-generator-architecture.md +++ b/docs/superpowers/specs/2026-08-09-generator-architecture.md @@ -1,6 +1,6 @@ # 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 @@ -132,18 +132,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 +213,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 +271,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 +312,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 +333,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 +364,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 +375,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 +472,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 +584,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 +648,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 e443e535..153bba6f 100644 --- a/docs/superpowers/specs/2026-08-09-public-api-design.md +++ b/docs/superpowers/specs/2026-08-09-public-api-design.md @@ -1,6 +1,6 @@ # 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 @@ -96,7 +96,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 +206,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 +232,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 +449,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 +635,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 1d81937e..48498a9a 100644 --- a/docs/superpowers/specs/2026-08-10-testing-architecture-design.md +++ b/docs/superpowers/specs/2026-08-10-testing-architecture-design.md @@ -1,6 +1,6 @@ # 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 @@ -483,7 +483,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 +537,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 +577,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 64b562f0..ce783832 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,7 +618,6 @@ 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` @@ -626,6 +627,5 @@ design and revised execution plan. - `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` From d0865e9aa823f8e3f0da6e869472514ef04dc156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 14:07:50 +0300 Subject: [PATCH 03/28] feat(generator): land Arc B model policy and seal the v2 retarget Model policy (Arc B Task 1, first increment): - CSharpNamePolicy: ordinary PascalCase for acronym tokens (Id, SessionId, Url) - OptionalCollectionInput: absent optional collections read empty while explicit JSON null still throws; generated accessors route through the nullable-input helper; emitter, snapshot, and serialization tests added - regenerate the selected model closure with the new casing - analyzer-driven formatting normalization in tool sources Direction (sealed 2026-08-13): the SDK targets the v2 protocol surface only. - ADR-0005 revised in place (v2-protocol-surface-only); ADR-0006/0008 trimmed - research doc 15 (v2 platform), research log session 17, docs 09/10 corrections - ROADMAP: M5 legacy hub removed, M3/M4 re-scoped, open questions refreshed - Arc B plan gains Task 0 (spec pin retarget); handoff and glossary updated --- AGENTS.md | 16 +- CONTEXT.md | 8 +- docs/ROADMAP.md | 51 +- docs/adr/0005-both-api-surfaces.md | 29 - docs/adr/0005-v2-protocol-surface-only.md | 32 + docs/adr/0006-monorepo-and-versioning.md | 2 +- docs/adr/0008-generated-operation-methods.md | 11 +- .../handover-prompts/HANDOFF-2026-08-13.md | 604 ++++++++++++++++++ docs/research/00-research-log.md | 100 +++ docs/research/09-upstream-v1v2.md | 7 +- .../10-v2-to-2.0-operation-mapping.md | 15 +- docs/research/15-opencode-v2-platform.md | 261 ++++++++ .../2026-08-13-m1-arc-b-callable-client.md | 54 +- .../2026-08-09-generator-architecture.md | 4 + .../specs/2026-08-09-public-api-design.md | 4 + .../2026-08-10-testing-architecture-design.md | 4 + ...8-11-production-walking-skeleton-design.md | 2 +- src/OpenCode.Sdk/.generated-manifest.json | 1 + .../Serialization/OptionalCollectionInput.cs | 9 + .../Models/MessageNotFoundError.cs | 4 +- src/OpenCode.Sdk/Models/ModelRef.cs | 4 +- .../Models/SessionMessageAgentSwitched.cs | 13 +- .../Models/SessionMessageAssistant.cs | 13 +- .../SessionMessageAssistantReasoning.cs | 13 +- .../Models/SessionMessageAssistantSnapshot.cs | 11 +- .../Models/SessionMessageAssistantText.cs | 2 +- .../Models/SessionMessageAssistantTool.cs | 2 +- .../SessionMessageAssistantToolProvider.cs | 21 +- .../Models/SessionMessageCompaction.cs | 13 +- .../Models/SessionMessageModelSwitched.cs | 13 +- .../Models/SessionMessageShell.cs | 15 +- .../Models/SessionMessageSynthetic.cs | 15 +- .../Models/SessionMessageSystem.cs | 13 +- .../SessionMessageToolStateCompleted.cs | 21 +- src/OpenCode.Sdk/Models/SessionMessageUser.cs | 33 +- .../Models/SessionNotFoundError.cs | 2 +- .../Serialization/OpenCodeJsonContextTests.cs | 18 +- .../Binding/CSharpNamePolicyTests.cs | 20 + .../Generator/Emission/ModelEmitterTests.cs | 9 + .../OptionalCollectionInputEmitterTests.cs | 15 + ...ce_Immutable_Annotated_Models.verified.txt | 21 +- ...The_Nullable_Input_Normalizer.verified.txt | 11 + .../Commands/GenerateCommand.cs | 31 +- .../Binding/BindingErrorCollector.cs | 1 + .../Generator/Binding/CSharpNamePolicy.cs | 6 - .../Generator/Binding/CurationValidator.cs | 6 +- .../Binding/ReachableSchemaCollector.cs | 3 +- .../Generator/Binding/SchemaNameResolver.cs | 3 +- .../Generator/Binding/SchemaPlanBinder.cs | 9 +- .../Generator/Binding/SpecBinder.cs | 3 +- .../Generator/Emission/ModelEmitter.cs | 63 +- .../OptionalCollectionInputEmitter.cs | 50 ++ .../Generator/Emission/SourceEmitter.cs | 7 +- .../Generator/GenerationCoordinator.cs | 34 +- .../Ingestion/Projection/GraphKeyBuilder.cs | 1 + .../Generator/Ingestion/SpecIngestion.cs | 7 +- 56 files changed, 1387 insertions(+), 353 deletions(-) delete mode 100644 docs/adr/0005-both-api-surfaces.md create mode 100644 docs/adr/0005-v2-protocol-surface-only.md create mode 100644 docs/agents/handover-prompts/HANDOFF-2026-08-13.md create mode 100644 docs/research/15-opencode-v2-platform.md create mode 100644 src/OpenCode.Sdk/Internal/Serialization/OptionalCollectionInput.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/OptionalCollectionInputEmitterTests.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Snapshots/OptionalCollectionInputEmitterTests.Emit_Should_Produce_The_Nullable_Input_Normalizer.verified.txt create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Emission/OptionalCollectionInputEmitter.cs diff --git a/AGENTS.md b/AGENTS.md index 9cb664df..07efd397 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); the 1.x legacy surface is never built (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 diff --git a/CONTEXT.md b/CONTEXT.md index 3a74725e..e0d769aa 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -59,13 +59,13 @@ 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. +**Protocol surface** (historically "modern surface"): +The `v2.*`-prefixed protocol operation block — the only surface this SDK generates +(ADR-0005); 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. +The un-prefixed operation block of the 1.x spec; never built (ADR-0005). **Launcher**: The in-core component that starts, monitors, and stops a local `opencode serve` process. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 764e4e36..7b1ed458 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -12,8 +12,11 @@ SpecIR ingestion are in place. M1's compiler arc selects the two walking-skeleto 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 active work is M1 Arc B's minimal transport and callable client with typed errors and -`NoThrow`; see the just-in-time + +Direction sealed 2026-08-13: the SDK targets the **v2 protocol surface only** (ADR-0005; +platform evidence: research doc 15). `spec/` still holds the 1.18.15 pin until the retarget +task at the head of M1 Arc B executes. The active work is M1 Arc B: the retarget plus the +minimal transport and callable client with typed errors and `NoThrow`; see the just-in-time [M1 Arc B callable-client plan](superpowers/plans/2026-08-13-m1-arc-b-callable-client.md). ## Milestones @@ -25,40 +28,48 @@ 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. +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. +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. ## 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. +- **Location addressing** — v2 operations take `location[...]` query parameters where the + v1 design leaned on the `x-opencode-directory` header; the client/per-call directory + decoration policy is decided in Arc B's retarget task. - **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/0005-both-api-surfaces.md b/docs/adr/0005-both-api-surfaces.md deleted file mode 100644 index 387b94d4..00000000 --- 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 00000000..7bff375b --- /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 c2b212b1..0a53e484 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/0008-generated-operation-methods.md b/docs/adr/0008-generated-operation-methods.md index e45deb7f..ae0bcccd 100644 --- a/docs/adr/0008-generated-operation-methods.md +++ b/docs/adr/0008-generated-operation-methods.md @@ -2,7 +2,7 @@ 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,8 +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. -- Bound handles are opt-in modern group curation: a row declares a client name, handle name, +- 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: - modern operations carrying the parameter emit on the handle with it partially applied; - collection operations stay on the collection client. The legacy surface remains flat. - Emitters never branch on operation IDs, wire group names, or concrete client names. + 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.md b/docs/agents/handover-prompts/HANDOFF-2026-08-13.md new file mode 100644 index 00000000..57553500 --- /dev/null +++ b/docs/agents/handover-prompts/HANDOFF-2026-08-13.md @@ -0,0 +1,604 @@ +# M1 Arc B Callable Client Handoff + +Date: 2026-08-13 + +Consume this document against live Git and delete it when Arc B ships. It records unfinished +session state, not evergreen architecture. + +## Direction Update — v2 Retarget (sealed after this handoff was written) + +The SDK now targets the v2 protocol surface only (ADR-0005 as revised 2026-08-13; platform +evidence: `docs/research/15-opencode-v2-platform.md`). The canonical plan gained a Task 0 +that retargets the spec pin to a `v2`-branch snapshot before Task 1 continues. Consequences +for this handoff: the uncommitted Task 1 model-policy work remains valid and wanted (the +policy code and its tests are surface-independent; the 17 regenerated models regenerate +again from the v2 pin in Task 0); the "Selected Wire Contracts" section below describes the +retiring 1.18.15 pin — Task 0 re-derives both operations' contracts (the health success +becomes the `ServiceHealth` object); the real-server demonstration targets `opencode2 serve` +(npm `@opencode-ai/cli@next`, already installed on this machine). + +## Objective + +Complete M1 Arc B: make `v2.health.get` and `v2.session.message` callable through their final +generated SDK surface, backed by a minimal hand-written HTTP core with typed errors and per-call +`NoThrow`, then demonstrate both calls against a real `opencode serve`. + +The canonical execution plan is +`docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md`. Read it before changing code. +The binding decisions are `AGENTS.md` and ADRs 0003, 0004, 0007, 0008, and 0009. The files under +`docs/superpowers/specs/` are reference material, not binding law. + +## Git And Workspace State + +| Item | State | +|---|---| +| Workspace | `/home/deniz/src/opencode-sdk-dotnet` | +| Branch | `feature/m1-callable-client` | +| HEAD | `7f317f0 docs(plan): define M1 callable client arc` | +| Branch base | `f35f741 docs(research): inventory Polyfill capabilities` | +| Remote base | `origin/master` at `3e3e058` | +| Ahead of `origin/master` | 2 commits | +| Remote feature branch | None | +| Pushes | None | +| Staging area | Empty | +| Worktree | Dirty: partial Task 1 model-policy implementation | + +The maintainer said `commit ve başla`, so implementation commits were authorized for this +development loop. Do not push without a new explicit request. SSH operations in this environment +may require `GIT_SSH_COMMAND='ssh -F /dev/null'`. + +Commit `7f317f0` is complete and should be retained. It adds the Arc B plan, updates the ROADMAP, +aligns ADR-0004/0007/0008, updates the research chain, removes the stale broad M1 plan, and +corrects affected reference documents. Before Task 1 code started, that docs state passed Release +build, 230 tests, format, Slopwatch, and `git diff --check`. + +## Current Uncommitted Work + +A delegated worker implemented the model-policy portion of Task 1. Nothing is staged or +committed. Do not run a blanket checkout/reset: inspect and preserve the intended work first. + +### Intended model-policy changes + +The intended change set does the following: + +- Changes `CSharpNamePolicy` from the old two-character-uppercase special case to ordinary + PascalCase: `id` -> `Id`, `sessionID` -> `SessionId`, `messageID` -> `MessageId`, + `callID` -> `CallId`, `URL` -> `Url`, and `APIError` -> `ApiError`. +- Adds focused casing tests in + `tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs`. +- Replaces direct `value is null` checks in non-null optional collection accessors with a + generated nullable-input helper, preserving a non-null public getter. +- Adds `OptionalCollectionInputEmitter` and generated + `src/OpenCode.Sdk/Internal/Serialization/OptionalCollectionInput.cs`. +- Updates `SourceEmitter` and `.generated-manifest.json` to own the helper. +- Adds an emitter snapshot and a focused assertion that optional non-null collections route + through the helper. +- Regenerates the selected model closure with `Id`-style members and expression-bodied init + accessors. +- Updates SDK serialization tests to prove absent optional collections become empty while the + existing explicit-null fixture still throws. + +The helper currently emits this shape: + +```csharp +internal static class OptionalCollectionInput +{ + internal static T Normalize(T? value, T empty, Func copy) + where T : class => value is null ? empty : copy(value); +} +``` + +Generated properties use the already initialized backing field as the empty value: + +```csharp +public IReadOnlyDictionary Metadata +{ + get; + init => field = OptionalCollectionInput.Normalize(value, field, static input => ...); +} = ...; +``` + +This is intentional. System.Text.Json's source-generated record path may pass null through the +init path for an absent optional collection. The helper's input is honestly nullable, while the +public property stays non-null. With `RespectNullableAnnotations=true`, explicit wire null is +rejected before assignment. Do not add `[AllowNull]`: a probe showed it also admits explicit JSON +null, violating the pinned schema. + +### Files belonging to the intended partial work + +| Area | Files | +|---|---| +| Naming | `CSharpNamePolicy.cs`, `CSharpNamePolicyTests.cs` | +| Collection emission | `ModelEmitter.cs`, `OptionalCollectionInputEmitter.cs`, `SourceEmitter.cs` | +| Emitter tests | `ModelEmitterTests.cs`, `OptionalCollectionInputEmitterTests.cs`, both affected snapshots | +| Product tests | `OpenCodeJsonContextTests.cs` | +| Generated output | `OptionalCollectionInput.cs`, `.generated-manifest.json`, 17 changed model files | + +The changed generated models include `MessageNotFoundError`, `ModelRef`, +`SessionMessageAgentSwitched`, `SessionMessageAssistant`, assistant content/tool/provider models, +`SessionMessageCompaction`, switched/shell/synthetic/system/user models, +`SessionMessageToolStateCompleted`, and `SessionNotFoundError`. + +### Formatter/analyzer collateral requiring review + +The dirty worktree also contains unrelated edits in these files: + +```text +tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs +tools/OpenCode.Sdk.Tools/Generator/Binding/BindingErrorCollector.cs +tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs +tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs +tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs +tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs +tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs +tools/OpenCode.Sdk.Tools/Generator/GenerationCoordinator.cs +tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs +tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs +``` + +Most are formatting/local-flow changes. Two replace `GroupBy(...).Count() > 1` with +`Skip(1).Any()`, which is behaviorally equivalent and likely analyzer-driven. They are not needed +for the model-policy change. Review them individually; either discard them before the first code +commit or isolate them in a justified refactor. Do not accidentally mix them into operation +binding review. + +### Reported verification of the partial work + +The delegated worker reported: + +| Check | Result | +|---|---| +| Focused Tools tests | 12 passed | +| SDK serialization tests | 13 passed on each of net8.0, net9.0, net10.0 | +| `generate --verify` | Current | +| `dotnet build OpenCode.slnx --no-restore` | 0 warnings, 0 errors | +| `dotnet format OpenCode.slnx --no-restore --verify-no-changes` | Passed | +| Slopwatch | 0 issues | +| `git diff --check` | Passed | + +These were not converted into a commit. After cleaning the collateral, rerun the full repository +tests, not only the focused set, before committing. + +Scratch state is non-authoritative. `.scratchpad/Directory.Build.props` exists as the required +stub; `.scratchpad/stj-nullability-probe/` retains only ignored `bin/` and `obj/`; the source probe +was deleted. `.scratchpad/worktrees/` is empty. + +## What Has Not Started + +No operation/client binding code has been written. Specifically: + +- `tools/curation.json` has no `handleParameter` yet. +- `GroupCuration` has no matching property. +- `EmitPlan` still contains only selected IDs, models, unions, registry, and pending operations. +- There are no `ClientPlan`, `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, or + `ErrorMapPlan` types. +- There is no `OperationPlanBinder`. +- There are no client, route, envelope, operation-method, or response-adapter emitters. +- There is no hand-written pipeline, option/response/exception spine, or SDK friend assembly. +- `OpenCode.Sdk.csproj` has no net472 `System.Net.Http` framework reference. +- `GenerationWriter` still owns only `Models/` and `Internal/Serialization/`. +- `CS1591` is still disabled. +- `PublicApiGenerator` is not pinned or referenced. +- There is no generated `OpenCodeClient`, `SessionsClient`, `SessionClient`, response, or route. +- No real-server demonstration has run. + +## Locked And Approved Arc B Decisions + +### Generator boundary + +This is an opencode-specific generator, not a universal OpenAPI generator. It may fail closed on +opencode dialect shapes outside the current selected capability. However, endpoint and client +names must not be hard-coded in emitters or operation-ID branches. + +The intended flow is: + +```text +SpecIR + curation + -> Binder policy + -> final Client/Operation/Envelope/Error plans + -> dumb Roslyn emitters +``` + +Emitters must not know `v2.session.message`, `health`, `session`, or `SessionClient`. They render +final plan values. A synthetic operation with the same supported shape but different names must +bind and emit without a new production branch. + +### Bound handle + +`SessionsClient` is the collection facade. It will eventually carry collection operations such +as list/create and currently exposes `GetSessionClient`. + +`SessionClient` is a bound handle containing only the shared pipeline and immutable session ID. +It caches no server state. Session-scoped operations exist only on this handle; there is no +duplicate flat overload. + +OpenAPI cannot infer that `sessionID` should be partially applied. This is public API taste and +therefore curation. Update the modern `session` row to: + +```json +{ + "placement": "client", + "clientName": "Sessions", + "handleName": "SessionClient", + "handleParameter": "sessionID" +} +``` + +The Binder applies one general rule to any modern group declaring a handle. Legacy remains flat, +even if a legacy route contains `sessionID`. + +### Identifier casing + +Use ordinary PascalCase for all acronym tokens, regardless of length. Wire spelling remains in +`JsonPropertyName`. Brand spellings such as `OAuth` remain curated exceptions when reached. + +### Collection nullability + +Requiredness and nullability are independent. For an optional non-null collection: + +| Wire | SDK result | +|---|---| +| Property absent | Immutable empty collection | +| Property present with collection/object | Immutable defensive copy | +| Property present as explicit null | `JsonException` | + +Do not make the public property nullable merely because the deserializer's assignment path may +use null for absence. + +### HTTP and endpoint policy + +- Add `` conditionally for net472. Do not add the old + `System.Net.Http` NuGet package. Add the same framework reference to the net472 test path only + if test code directly needs it. +- Use `HttpClient.SendAsync(HttpRequestMessage, ResponseHeadersRead, CancellationToken)` as the + only send primitive. +- URI constructors make their URI authoritative. Reject a conflicting non-null + `OpenCodeClientOptions.Endpoint`. +- BYO `HttpClient` requires `options.Endpoint`. +- Never read or mutate `HttpClient.BaseAddress` or `DefaultRequestHeaders`. +- Accept only absolute HTTP(S) endpoint URIs without query or fragment; preserve a normalized + endpoint path prefix when joining generated routes. +- Self-created `HttpClient` is owned and disposed. Injected `HttpClient` is never disposed. +- Disposing the root still marks every SDK wrapper unusable. + +### Decoration + +- Resolve password once at construction: explicit option, then + `OPENCODE_SERVER_PASSWORD`, then no auth. +- Basic authentication uses user `opencode`. +- Per-call directory overrides client directory and is sent as `x-opencode-directory`. +- Every request carries `OpenCode.Sdk/` User-Agent. +- Decorate each request, never shared client headers. + +The exact safe extraction/sanitization of the informational version still needs an internal +implementation decision and tests. Do not silently substitute assembly version without noting +the behavioral difference. + +### Error channels + +API errors throw by default. `OpenCodeRequestOptions.NoThrow` is the only switch and applies per +call. It suppresses API-status exceptions only; usage, disposal, cancellation, transport, and +protocol failures still throw. + +`OpenCodeResponse` carries `Status`, `IsError`, typed `Error`, and nullable error-path `RawBody`. +Throwing API failures carry the same status/error/raw body on `OpenCodeApiException`. + +Only a tag allowed by that operation's status map becomes its concrete generated error type. An +unknown tag, undeclared status, or known tag returned at the wrong status becomes +`UnknownOpenCodeError` with exact tag and raw `JsonElement`. Malformed non-2xx JSON yields a null +typed error while preserving exact raw body. Unexpected or malformed 2xx is a protocol failure +wrapped as `OpenCodeTransportException`. Every `OperationCanceledException` passes through. + +## Selected Wire Contracts + +### `v2.health.get` + +| Fact | Value | +|---|---| +| Method/path | `GET /api/health` | +| Parameters/body | None | +| 200 | JSON `{ "healthy": true }`, classified `Bare` | +| 400 | `InvalidRequestError` | +| 401 | `UnauthorizedError` | +| Public owner/method | `OpenCodeClient.GetHealthAsync` | +| Response/payload | `HealthResponse.Healthy : bool` | + +The success root is promoted under: + +```text +op:v2.health.get#/responses/200/content/application~1json/schema +``` + +The pinned boolean is a one-value literal but intentionally emits `bool`, preserving a future +false value rather than rewriting it. + +### `v2.session.message` + +| Fact | Value | +|---|---| +| Method/path | `GET /api/session/{sessionID}/message/{messageID}` | +| Bound parameter | required path string `sessionID` | +| Method parameter | required path string `messageID` | +| Body/query | None | +| 200 | JSON `{ "data": SessionMessage }`, classified `Data` | +| 400 | `InvalidRequestError` | +| 401 | `UnauthorizedError` | +| 404 | `MessageNotFoundError` or `SessionNotFoundError` | +| Public owner/method | `SessionClient.GetMessageAsync(messageId, ...)` | +| Response/payload | `SessionMessageResponse.Message : SessionMessage` | + +The response roots are: + +```text +op:v2.session.message#/responses/200/content/application~1json/schema +op:v2.session.message#/responses/404/content/application~1json/schema +``` + +The raw 404 `anyOf` repeats `SessionNotFoundError`. Existing union normalization deduplicates it, +so the Binder must consume the semantic two-type set and must not re-read raw JSON to recover the +discarded duplicate. + +Relevant pinned source locations are `spec/openapi.json:9780` and +`spec/openapi.json:11449`. + +## Operation Binding Blueprint + +The following is a recommended minimal shape, not committed code. Keep it consumed-only and +adjust names only when an emitter demonstrably needs a different field. + +| Plan | Minimum consumed facts | +|---|---| +| `ClientPlan` | root property name, collection client type, optional handle type/factory, handle parameter | +| `OperationParameterPlan` | wire name, final C# name, `TypeReferencePlan`, handle-bound flag | +| `EnvelopePlan` | response type, success status/media, wire payload name, public payload name/type, docs | +| `ErrorVariantPlan` | wire tag and concrete generated type | +| `ErrorMapPlan` | status, media type, allowed error variants | +| `OperationPlan` | operation ID, owner type, method, HTTP method, route names/template, parameters, response/adapter, error maps, docs | + +Expected final values include: + +| Operation | Owner | Method | Response | Adapter | +|---|---|---|---|---| +| Health | `OpenCodeClient` | `GetHealthAsync` | `HealthResponse` | `HealthResponseAdapter` | +| Message | `SessionClient` | `GetMessageAsync` | `SessionMessageResponse` | `SessionMessageResponseAdapter` | + +The public route member names are not yet mechanically represented. Settle them before emission +because `OpenCodeRoutes` enters the API baseline. A coherent reference shape is nested +`OpenCodeRoutes.Health.Get` and `OpenCodeRoutes.Sessions.GetMessage(sessionId, messageId)`. +Nested public route classes trigger CA1034; use the repository's narrow, commented arbitration +pattern rather than weakening the rule globally. + +### Existing seam problem + +`SchemaPlanBinder` currently resolves final schema names internally and keeps `TypePlanBinder` as +a private nested class. `SchemaBindingResult` discards the name map. Operation binding needs the +same final types for path parameters and response payloads without duplicating schema policy. + +Recommended direction: + +1. Resolve schema names once per `SpecBinder.Bind` invocation. +2. Pass the same resolved map to both schema and operation binding. +3. Extract the nested type binder into a named internal collaborator or narrowly expose a + reusable type-resolution service. +4. Keep emitters entirely free of SpecIR and curation. + +Do not solve this by letting an operation emitter inspect schemas or by matching operation IDs. + +### Validation algorithm + +For each selected operation, bind or report located errors for all of these facts: + +1. Surface and placement: M1 supports modern ordinary operations; modern group curation chooses + root, collection, or declared handle. Legacy placement stays flat. +2. Transport shape: GET, no body, no query, no SSE/WebSocket/wildcard, one 200 success, JSON + response bodies. +3. Path integrity: every route token has exactly one required path parameter; M1 parameters + resolve to non-null string. +4. Handle binding: a declared handle parameter is removed from the method signature and stored + on the handle; remaining parameters stay on the generated method. +5. Success root: verify required properties and forbidden additional properties rather than + trusting envelope classification alone. +6. Error map: direct refs become one allowed variant; response-root marked unions flatten to a + deduplicated tag/type list; all concrete errors use the generated error hierarchy. +7. Naming: final owner, method, response, route, adapter, parameter, and payload names collide + nowhere in the selected output. +8. Ordering: clients/names ordinal, statuses numeric, variants ordinal by tag, generated paths + ordinal. + +Add an operation/protocol binding error category rather than mislabeling route/status/media +failures as schema errors. + +### Curation changes + +Add `[JsonPropertyName("handleParameter")] public string? HandleParameter` to `GroupCuration`. +Update curation validation so: + +- Root placement rejects client, handle, and handle-parameter fields. +- Client placement requires `clientName`. +- `handleName` and `handleParameter` are paired. +- The handle parameter names a required path parameter on at least one selected modern operation + in that group. +- Modern group coverage is selection-scoped during staged generation. +- Curation-to-spec orphan checks remain global. +- Legacy operations do not require or consume modern group-handle curation. + +Remove the current hard-coded `sessionID` detection from `CurationValidator`. + +## Runtime Core Blueprint + +Hand-written identity core: + +```text +ErrorBehavior.cs +OpenCodeClientOptions.cs +OpenCodeRequestOptions.cs +OpenCodeResponse.cs +OpenCodeException.cs +OpenCodeApiException.cs +OpenCodeTransportException.cs +Internal/Abstractions/IEnvironmentProvider.cs +Internal/SystemEnvironmentProvider.cs +Internal/Pipeline.cs +Internal/ResponseAdapter.cs +Properties/AssemblyInfo.cs +``` + +Generated callable surface: + +```text +OpenCodeClient.cs +SessionsClient.cs +SessionClient.cs +HealthResponse.cs +SessionMessageResponse.cs +OpenCodeRoutes.cs +Internal/ResponseAdapters/HealthResponseAdapter.cs +Internal/ResponseAdapters/SessionMessageResponseAdapter.cs +``` + +The agreed pipeline signature is: + +```csharp +Task ExecuteAsync( + HttpMethod method, + string route, + ResponseAdapter adapter, + OpenCodeRequestOptions? options, + CancellationToken cancellationToken) + where TResponse : OpenCodeResponse; +``` + +A small implementation is to buffer the M1 JSON body in `Pipeline`, then invoke a synchronous +adapter that maps `(status, rawBody)` to a typed envelope. Pipeline applies default throw versus +`NoThrow` after adaptation. This keeps HTTP ownership/channel policy in one place and +operation-specific JSON/status rules in generated adapters. + +Generated operation methods perform guards and one delegation only. `OperationMethodEmitter` +should return Roslyn method members consumed by `ClientEmitter`; do not emit partial operation +files, because file/type analyzers require one owner per client type. + +The exception constructors need deliberate treatment under CA1032 before the first API baseline. +Do not weaken the analyzer globally. The protected mock constructors must produce an instructive +`InvalidOperationException` when an unoverridden member reaches an absent pipeline. + +## Generated Ownership Changes + +`GenerationWriter.ValidateGeneratedSourcePath` currently admits only `Models/` and +`Internal/Serialization/`. Arc B needs root generated types and +`Internal/ResponseAdapters/` beside hand-written files. + +Before broadening the path policy: + +- Refuse writing an existing file that is not in the previous manifest. +- Require the exact two-line generator provenance header before overwriting or deleting a + manifest-owned source. +- Admit root generated `.cs` paths and the dedicated adapter subtree, not arbitrary + `Internal/` files. +- Preserve unsafe-path and symlink refusal. +- Treat duplicate generated paths case-insensitively at the `SourceEmitter` boundary, matching + writer ownership semantics. +- Test stale root/adapter deletion and preservation of hand-written root/internal files. + +`generate --verify` repairs owned drift and then reports it through before/after byte comparison; +it does not depend on Git. + +## Contract Test Matrix + +Use the real generated clients with a small recording `HttpMessageHandler`. Mock only the +environment seam. Keep wire JSON in embedded fixtures, never inline dumps. + +| Area | Required scenarios | +|---|---| +| Health | 200, 400, 401 under default and `NoThrow` | +| Message | 200, 400, 401, both 404 variants under default and `NoThrow` | +| Error skew | unknown tag, undeclared status, known tag at wrong status, malformed body | +| Envelopes | payload guard on error, `ToString` does not trip guarded getter, exact raw body | +| Routes | escape slash, space, percent, question mark in both IDs | +| Endpoint | trailing slash, no trailing slash, preserved path prefix, rejected query/fragment | +| Auth | explicit, environment fallback, explicit wins, one-time read, absent password | +| Directory | client default, per-call override, absent header | +| User-Agent | exact SDK product/version token on both operations | +| Cancellation | pre-cancelled and handler-blocked token; exception remains cancellation | +| Transport | `HttpRequestException`, malformed/unexpected success, response disposal | +| Ownership | owned client disposed; BYO client remains usable; wrappers reject use after root disposal | +| Mock seam | protected-constructor overrides and instructive unoverridden-member failure | +| API | one reviewed PublicApiGenerator/Verify baseline | + +Tests run on net8/net9/net10 locally; net472 runs on Windows CI and proxies netstandard2.0 runtime +behavior. `tests/OpenCode.Sdk.Tests/App.net472.config` is test-executable-only and must never flow +into the package. + +## Remaining Repository Work + +1. Clean and commit the current model-policy diff after full verification. +2. Implement curation-driven operation plans and Binder validation. +3. Add the net472 framework reference and hand-written HTTP core. +4. Add client/envelope/route/adapter emitters and generated ownership hardening. +5. Enable CS1591 for shipped source and document every hand-written public member. +6. Pin `PublicApiGenerator` 11.5.4 and add `Verify.TUnit` to SDK tests. +7. Add Linux CI `generate --verify` before build. +8. Regenerate, review the first public API baseline, and retain the partial packaging marker. +9. Run the complete repository gates. +10. Perform the real-server demonstration and move ROADMAP active work to M2. + +## Real-Server Demonstration + +`opencode` is installed at `/home/deniz/.nvm/versions/node/v24.18.0/bin/opencode`; the installed +version is 1.18.18 while the pinned spec snapshot is 1.18.15. Treat that version skew as visible +demo evidence; do not silently claim exact-pin execution. + +Start a password-enabled server in a directory containing at least one session message. Obtain +IDs with authenticated raw calls: + +```text +GET /api/session?limit=1 +GET /api/session/{sessionID}/message?limit=1 +``` + +Then use a temporary ignored console app to call only the generated health and single-message +methods and print `Healthy`, the concrete message type, and message `Id`. Paste exact commands and +output into the PR description. No automated process harness belongs in M1. + +## Final Gates + +Run these from the repository root: + +```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. Pack must fail only because +`src/OpenCode.Sdk/.generation-incomplete` records pending operations. + +## Stop Conditions + +Stop and involve the maintainer if any of these occurs: + +- Binder needs Microsoft.OpenApi or raw JSON to recover a fact SpecIR claims to provide. +- An emitter needs an operation ID, wire group, or concrete client-name branch. +- Supporting either selected operation needs a temporary public or transport API. +- The selected closure reaches a structural union. +- A selected response shape exceeds the declared M1 capability rather than failing closed. +- Generated ownership cannot distinguish hand-written files safely. +- Any product source fails on a locked target framework. +- Partial generation can pack successfully. +- Retry, telemetry, DI, launcher, SSE, or breadth becomes necessary merely to call the two M1 + operations. + +## Recommended First Actions For The Next Session + +1. Read `AGENTS.md`, the Arc B plan, ADR-0004/0007/0008, and this handoff. +2. Inspect `git status`, `git diff`, and commit `7f317f0`; do not assume the dirty files are all + intentional. +3. Separate the model-policy work from global formatter/analyzer collateral. +4. Run full build/tests/format/Slopwatch and commit the cleaned model-policy increment. +5. Red-test `handleParameter`, legacy-flat placement, the two pinned operation plans, and a + synthetic same-shape operation before implementing `OperationPlanBinder`. +6. Continue in the canonical plan's Task 1 order; delete this handoff only when the follow-up + work ships. diff --git a/docs/research/00-research-log.md b/docs/research/00-research-log.md index 0bf54d9f..e9535525 100644 --- a/docs/research/00-research-log.md +++ b/docs/research/00-research-log.md @@ -1509,3 +1509,103 @@ helper preserves empty-on-absence while STJ rejects explicit null before assignm **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. diff --git a/docs/research/09-upstream-v1v2.md b/docs/research/09-upstream-v1v2.md index da528b63..8cb35351 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 2b9f4b87..a409f57a 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/15-opencode-v2-platform.md b/docs/research/15-opencode-v2-platform.md new file mode 100644 index 00000000..6b072092 --- /dev/null +++ b/docs/research/15-opencode-v2-platform.md @@ -0,0 +1,261 @@ +# 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`; no `x-effect-stream` marker remains. 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-13-m1-arc-b-callable-client.md b/docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md index 005d08e5..88ad93a6 100644 --- 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 @@ -14,10 +14,10 @@ names, or concrete client names. Generated methods delegate once into a hand-wri - 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 modern `session` row declares `Sessions`, +- 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. Legacy operations remain flat and pending in M1. + operation. All other operations stay pending in M1. - 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. @@ -37,6 +37,29 @@ names, or concrete client names. Generated methods delegate once into a hand-wri - 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). + +- [ ] 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). +- [ ] 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. +- [ ] 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 are re-read from the v2 responses — do not assume the 1.x tables. +- [ ] Decide dotted-name mapping for the selected closure (`Session.Message.Info` and + friends): mechanical mangling, curated overrides only where the mechanical name misfires. +- [ ] Decide the directory/location decoration policy: v2 addresses per-request scope with + `location[...]` query parameters; the `x-opencode-directory` header plan is v1-derived. +- [ ] 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, @@ -48,19 +71,19 @@ and committed generated models. - [ ] 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. -- [ ] Add `handleParameter` to modern group curation. Require it and `handleName` together, - validate it names a required path parameter, and never consume a query/body value. Keep all - pending legacy operations flat. +- [ ] 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. - [ ] Add consumed-only `ClientPlan`, `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, and `ErrorMapPlan` plus an `OperationPlanBinder`. Reuse schema-resolved type names instead of reinterpreting schemas. -- [ ] Bind only modern ordinary GET + JSON + path-parameter operations with one 200 success and - no body/query/stream/wildcard. Bind `{healthy}` to `HealthResponse.Healthy`, curated `{data}` - to `SessionMessageResponse.Message`, and the normalized 404 union to two semantic variants. +- [ ] 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`, curated + `{data}` to `SessionMessageResponse.Message`, and the error maps Task 0 derived. - [ ] 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; a legacy operation carrying `sessionID` must remain - flat. + 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 @@ -117,11 +140,12 @@ and committed generated models. `.generation-incomplete`; packing must still fail only because breadth is pending. - [ ] Run `generate`, clean `generate --verify`, Release build, supported test legs, format, Slopwatch, and intentional pack refusal. -- [ ] Start a password-enabled real `opencode serve` in a directory containing a message. Obtain - IDs with authenticated raw calls to `GET /api/session?limit=1` and - `GET /api/session/{sessionID}/message?limit=1`, then call only the two generated operations - from a temporary `.scratchpad/` console. Print `Healthy`, concrete message type, and message - `Id`; paste command/output into the PR and move `docs/ROADMAP.md` to M2. +- [ ] 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 diff --git a/docs/superpowers/specs/2026-08-09-generator-architecture.md b/docs/superpowers/specs/2026-08-09-generator-architecture.md index ce885c8a..67fcb3e1 100644 --- a/docs/superpowers/specs/2026-08-09-generator-architecture.md +++ b/docs/superpowers/specs/2026-08-09-generator-architecture.md @@ -5,6 +5,10 @@ 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 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 153bba6f..044f8371 100644 --- a/docs/superpowers/specs/2026-08-09-public-api-design.md +++ b/docs/superpowers/specs/2026-08-09-public-api-design.md @@ -5,6 +5,10 @@ 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 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 48498a9a..235bdcb8 100644 --- a/docs/superpowers/specs/2026-08-10-testing-architecture-design.md +++ b/docs/superpowers/specs/2026-08-10-testing-architecture-design.md @@ -5,6 +5,10 @@ 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 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 ce783832..a858d4d7 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 @@ -620,7 +620,7 @@ design and revised execution plan. - `docs/research/08-codegen-spike.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` diff --git a/src/OpenCode.Sdk/.generated-manifest.json b/src/OpenCode.Sdk/.generated-manifest.json index 185076f4..8a4f7b6d 100644 --- a/src/OpenCode.Sdk/.generated-manifest.json +++ b/src/OpenCode.Sdk/.generated-manifest.json @@ -3,6 +3,7 @@ "Internal/Serialization/LlmToolContentJsonConverter.cs", "Internal/Serialization/OpenCodeErrorJsonConverter.cs", "Internal/Serialization/OpenCodeJsonContext.cs", + "Internal/Serialization/OptionalCollectionInput.cs", "Internal/Serialization/SessionMessageAssistantContentJsonConverter.cs", "Internal/Serialization/SessionMessageJsonConverter.cs", "Internal/Serialization/SessionMessageToolStateJsonConverter.cs", diff --git a/src/OpenCode.Sdk/Internal/Serialization/OptionalCollectionInput.cs b/src/OpenCode.Sdk/Internal/Serialization/OptionalCollectionInput.cs new file mode 100644 index 00000000..5e22f011 --- /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/Models/MessageNotFoundError.cs b/src/OpenCode.Sdk/Models/MessageNotFoundError.cs index 4ee91c6b..20449fcf 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 6b7bb617..4c3cd523 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/SessionMessageAgentSwitched.cs b/src/OpenCode.Sdk/Models/SessionMessageAgentSwitched.cs index 092d3f0a..dce79b25 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAgentSwitched.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAgentSwitched.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; /// @@ -14,21 +15,13 @@ public sealed record SessionMessageAgentSwitched : SessionMessage /// 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. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistant.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistant.cs index d4a1b338..6262c40c 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistant.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistant.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; /// @@ -14,21 +15,13 @@ public sealed record SessionMessageAssistant : SessionMessage /// 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. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs index 31ee4287..d829f102 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; /// @@ -20,7 +21,7 @@ public sealed record SessionMessageAssistantReasoning : SessionMessageAssistantC /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the text value. @@ -32,15 +33,7 @@ public sealed record SessionMessageAssistantReasoning : SessionMessageAssistantC /// Gets the provider metadata 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)); + public IReadOnlyDictionary> ProviderMetadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary>(input.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 time value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantSnapshot.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantSnapshot.cs index 13914338..dd833832 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 491ce784..4124f6e5 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs @@ -18,7 +18,7 @@ public sealed record SessionMessageAssistantText : SessionMessageAssistantConten /// Gets the id value. /// [JsonPropertyName("id")] - public required string ID { get; init; } + public required string Id { get; init; } /// /// Gets the text value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs index 7013ccb2..dd792369 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantTool.cs @@ -18,7 +18,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. diff --git a/src/OpenCode.Sdk/Models/SessionMessageAssistantToolProvider.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantToolProvider.cs index 44f44229..a9bd895e 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantToolProvider.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantToolProvider.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; /// @@ -20,27 +21,11 @@ public sealed record SessionMessageAssistantToolProvider /// 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)); + public IReadOnlyDictionary> Metadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary>(input.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)); + public IReadOnlyDictionary> ResultMetadata { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary>(input.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/SessionMessageCompaction.cs b/src/OpenCode.Sdk/Models/SessionMessageCompaction.cs index de7ba3c0..ea456dcc 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageCompaction.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageCompaction.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; /// @@ -38,21 +39,13 @@ public sealed record SessionMessageCompaction : SessionMessage /// 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. diff --git a/src/OpenCode.Sdk/Models/SessionMessageModelSwitched.cs b/src/OpenCode.Sdk/Models/SessionMessageModelSwitched.cs index b40383fd..949da287 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageModelSwitched.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageModelSwitched.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; /// @@ -14,21 +15,13 @@ public sealed record SessionMessageModelSwitched : SessionMessage /// 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. diff --git a/src/OpenCode.Sdk/Models/SessionMessageShell.cs b/src/OpenCode.Sdk/Models/SessionMessageShell.cs index d382d087..b243331a 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageShell.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageShell.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; /// @@ -14,21 +15,13 @@ public sealed record SessionMessageShell : SessionMessage /// 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,7 +39,7 @@ public IReadOnlyDictionary Metadata /// Gets the call id value. /// [JsonPropertyName("callID")] - public required string CallID { get; init; } + public required string CallId { get; init; } /// /// Gets the command value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageSynthetic.cs b/src/OpenCode.Sdk/Models/SessionMessageSynthetic.cs index b617eb92..069d09fc 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageSynthetic.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageSynthetic.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; /// @@ -14,21 +15,13 @@ public sealed record SessionMessageSynthetic : SessionMessage /// 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. @@ -40,7 +33,7 @@ public IReadOnlyDictionary Metadata /// Gets the session id value. /// [JsonPropertyName("sessionID")] - public required string SessionID { get; init; } + public required string SessionId { get; init; } /// /// Gets the text value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageSystem.cs b/src/OpenCode.Sdk/Models/SessionMessageSystem.cs index b15265d9..a8035652 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageSystem.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageSystem.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; /// @@ -14,21 +15,13 @@ public sealed record SessionMessageSystem : SessionMessage /// 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. diff --git a/src/OpenCode.Sdk/Models/SessionMessageToolStateCompleted.cs b/src/OpenCode.Sdk/Models/SessionMessageToolStateCompleted.cs index c7fcfa05..4cef18dc 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; /// @@ -34,15 +35,7 @@ public required IReadOnlyDictionary Input /// 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(); - } - } = []; + public IReadOnlyList Attachments { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).AsReadOnly()); } = []; /// /// Gets the content value. @@ -62,15 +55,7 @@ public required IReadOnlyList Content /// 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(); - } - } = []; + public IReadOnlyList OutputPaths { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).AsReadOnly()); } = []; /// /// Gets the structured value. diff --git a/src/OpenCode.Sdk/Models/SessionMessageUser.cs b/src/OpenCode.Sdk/Models/SessionMessageUser.cs index 4987e920..419618ce 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageUser.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageUser.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; /// @@ -14,21 +15,13 @@ public sealed record SessionMessageUser : SessionMessage /// 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,13 @@ 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 type value. diff --git a/src/OpenCode.Sdk/Models/SessionNotFoundError.cs b/src/OpenCode.Sdk/Models/SessionNotFoundError.cs index 092b396b..ff348315 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/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs b/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs index ab7fbe3b..0f859fa5 100644 --- a/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs +++ b/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs @@ -18,13 +18,27 @@ public async Task Deserialize_Should_Create_Known_Outer_Variant_When_Marker_Is_L 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); 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(); + } + [Test] public async Task Deserialize_Should_Preserve_Unknown_Outer_Variant_Byte_For_Byte() { @@ -147,7 +161,7 @@ public async Task ProviderMetadata_Should_Recursively_Copy_Collections() }; var model = new SessionMessageAssistantReasoning { - ID = "part-1", + Id = "part-1", Text = "reasoning", ProviderMetadata = outer, }; 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 00000000..7c246ef0 --- /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/Emission/ModelEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ModelEmitterTests.cs index 4d958ab0..0fa518e8 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 00000000..2a2e6a5c --- /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/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 6a3f9913..49eeb69e 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 00000000..04724deb --- /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/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs b/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs index 11a65534..d5e372af 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,26 @@ 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 modern operations:[/] {report.PendingModernOperationIds.Count.ToString(CultureInfo.InvariantCulture)}"); + _console.MarkupLine($"[yellow]Pending legacy operations:[/] {report.PendingLegacyOperationIds.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 28cfd31c..5b18ae13 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 76dfd77b..f5943077 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 3ae1092c..085235f0 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs @@ -48,8 +48,7 @@ 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) { @@ -115,8 +114,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/ReachableSchemaCollector.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs index 21448a7c..ae97232d 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 98f370fd..b544d0a4 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); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs index a7a20691..0b9ce862 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs @@ -8,8 +8,7 @@ internal sealed class SchemaPlanBinder(SchemaNameResolver schemaNames) private const string ModelNamespace = "OpenCode.Sdk.Models"; private readonly SchemaNameResolver _schemaNames = schemaNames ?? throw new ArgumentNullException(nameof(schemaNames)); - public SchemaBindingResult Bind(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation, - BindingErrorCollector errors) + public SchemaBindingResult Bind(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(reachable); @@ -259,8 +258,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 97d329a3..61488a4f 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; @@ -48,7 +49,7 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera }; } - 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/ModelEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/ModelEmitter.cs index 3bd9b46d..63d39ffc 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/ModelEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/ModelEmitter.cs @@ -123,56 +123,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) @@ -297,6 +295,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); } 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 00000000..75cf3be7 --- /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/SourceEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs index dab9b9fd..f7d7ffb0 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs @@ -11,9 +11,12 @@ 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()); + + 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/GenerationCoordinator.cs b/tools/OpenCode.Sdk.Tools/Generator/GenerationCoordinator.cs index a27c09e4..c97a0fe6 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/GenerationCoordinator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/GenerationCoordinator.cs @@ -16,37 +16,48 @@ 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 + + var modern = plan + .PendingOperations .Where(static operation => operation.Surface == SpecSurface.Modern) .Select(static operation => operation.OperationId) .Order(StringComparer.Ordinal) .ToArray(); - var legacy = plan.PendingOperations + + var legacy = plan + .PendingOperations .Where(static operation => operation.Surface == SpecSurface.Legacy) .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 writeResult = await _writer + .WriteAsync( + request.OutputRoot, + request.ProjectPath, + SourceEmitter.Emit(plan), + marker, + request.Verify, + cancellationToken) + .ConfigureAwait(false); + return new GenerationReport { SelectedOperationIds = plan.SelectedOperationIds, @@ -69,6 +80,7 @@ private static string CreatePartialMarker(IReadOnlyList selected, string AppendOperations(content, "Selected", selected); AppendOperations(content, "Pending modern", modern); AppendOperations(content, "Pending legacy", legacy); + return content.ToString().ReplaceLineEndings("\n"); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs index d673f77f..1a50b67d 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/SpecIngestion.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs index 4160797d..07258d2c 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) From 5f71294535156045a83d1f196bbe523886bafcae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 14:16:31 +0300 Subject: [PATCH 04/28] docs(plan): seal retarget-task conventions - v1 ledger closed: location[...] query is the only per-request scoping; no x-opencode-directory, no backward-compatibility debt - schema names map mechanically; no taste curation (283/324 v2 schemas are dotted) - the Modern/Legacy surface split retires in Task 0 --- CONTEXT.md | 2 +- docs/ROADMAP.md | 3 --- docs/agents/handover-prompts/HANDOFF-2026-08-13.md | 6 +++++- .../plans/2026-08-13-m1-arc-b-callable-client.md | 14 ++++++++++---- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index e0d769aa..9e154141 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -52,7 +52,7 @@ 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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7b1ed458..de8e70a6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -58,9 +58,6 @@ is revisited at each milestone boundary. 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. -- **Location addressing** — v2 operations take `location[...]` query parameters where the - v1 design leaned on the `x-opencode-directory` header; the client/per-call directory - decoration policy is decided in Arc B's retarget task. - **Spec refresh cadence** — the `refresh-spec` tool lands in M6; the cadence policy stays open. - **Structural-union emission shape** — the v1 pin had five structural-union sites diff --git a/docs/agents/handover-prompts/HANDOFF-2026-08-13.md b/docs/agents/handover-prompts/HANDOFF-2026-08-13.md index 57553500..5819eb62 100644 --- a/docs/agents/handover-prompts/HANDOFF-2026-08-13.md +++ b/docs/agents/handover-prompts/HANDOFF-2026-08-13.md @@ -15,7 +15,11 @@ policy code and its tests are surface-independent; the 17 regenerated models reg again from the v2 pin in Task 0); the "Selected Wire Contracts" section below describes the retiring 1.18.15 pin — Task 0 re-derives both operations' contracts (the health success becomes the `ServiceHealth` object); the real-server demonstration targets `opencode2 serve` -(npm `@opencode-ai/cli@next`, already installed on this machine). +(npm `@opencode-ai/cli@next`, already installed on this machine). Sealed follow-ups +(2026-08-13, later): the v1 ledger is fully closed — no v1 convention survives, so the +`x-opencode-directory` decoration bullet below is void (v2 `location[...]` query is the only +scoping mechanism); schema names map mechanically (no taste curation — `Session.Message.Info` +becomes `SessionMessageInfo`); the Modern/Legacy surface split is retired in Task 0. ## Objective 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 index 88ad93a6..c54332ff 100644 --- 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 @@ -53,10 +53,16 @@ evidence: research doc 15). `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 are re-read from the v2 responses — do not assume the 1.x tables. -- [ ] Decide dotted-name mapping for the selected closure (`Session.Message.Info` and - friends): mechanical mangling, curated overrides only where the mechanical name misfires. -- [ ] Decide the directory/location decoration policy: v2 addresses per-request scope with - `location[...]` query parameters; the `x-opencode-directory` header plan is v1-derived. +- [ ] 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). +- [ ] 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). +- [ ] 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. - [ ] Regenerate the selected model closure; update curation rows, serialization tests, and fixtures to the v2 shapes; run the full repository gates. From 911b7f3b813db4f0fb3d162716a9163b4fc2740c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 15:11:56 +0300 Subject: [PATCH 05/28] feat(generator): retarget the pin to the opencode v2 protocol surface Task 0 of the M1 Arc B plan (ADR-0005 as revised; platform evidence: docs/research/15-opencode-v2-platform.md). - pin spec/openapi.json to v2-branch commit a6a712a (the protocol package's openapi.json); rewrite SNAPSHOT.md provenance - ingestion-wall admits, each fail-closed to the observed population: single-element validation-only allOf wrappers unwrap; single-value number enums project as number literals; a bare array projects as array-of-any; prefixItems admits exactly two shapes (closed tuple, NonEmptyArray encoding) and location pinning retires - union machinery for the v2 topology: discriminating-marker selection, nested marked unions (SessionMessageCompaction : SessionMessageInfo with a fixed outer tag), promoted inline branch naming (root + marker value), nested special-number encoding; unknown carriers emit wire-faithful JsonPropertyName and nested unknowns re-serialize their raw payload through the declared base type - retire the Modern/Legacy surface split: SpecSurface deleted, the parser requires the v2. protocol prefix fail-closed, single pending list in report/marker/output, legacy vocabulary swept from code and operational docs - regenerate the closure from the v2 pin (stale v1 files deleted by the manifest writer); remap curation to v2 names - update both test projects to the v2 surface with nested-union serialization coverage (221 tools + 45 SDK tests green) --- AGENTS.md | 2 +- CONTEXT.md | 11 +- docs/research/15-opencode-v2-platform.md | 3 +- .../2026-08-13-m1-arc-b-callable-client.md | 21 +- spec/SNAPSHOT.md | 27 +- spec/openapi.json | 43808 ++++++---------- src/OpenCode.Sdk/.generated-manifest.json | 63 +- src/OpenCode.Sdk/.generation-incomplete | 208 +- .../Serialization/OpenCodeJsonContext.cs | 55 +- .../PromptFileSourceJsonConverter.cs | 68 + .../SessionMessageCompactionJsonConverter.cs | 69 + ....cs => SessionMessageInfoJsonConverter.cs} | 34 +- .../SessionMessageToolStateJsonConverter.cs | 4 +- ...nverter.cs => ToolContentJsonConverter.cs} | 22 +- src/OpenCode.Sdk/Models/LocationRef.cs | 23 + .../Models/PromptAgentAttachment.cs | 6 +- .../Models/PromptFileAttachment.cs | 18 +- ...{LlmToolContent.cs => PromptFileSource.cs} | 6 +- .../Models/PromptFileSourceInline.cs | 16 + .../Models/PromptFileSourceUri.cs | 22 + .../{PromptSource.cs => PromptMention.cs} | 4 +- .../Models/PromptSkillAttachment.cs | 35 + src/OpenCode.Sdk/Models/ServiceHealth.cs | 28 + ...ched.cs => SessionMessageAgentSelected.cs} | 13 +- ....cs => SessionMessageAgentSelectedTime.cs} | 4 +- .../Models/SessionMessageAssistant.cs | 15 +- .../Models/SessionMessageAssistantFinish.cs | 42 + .../SessionMessageAssistantReasoning.cs | 12 +- .../Models/SessionMessageAssistantRetry.cs | 28 + .../Models/SessionMessageAssistantText.cs | 15 +- .../Models/SessionMessageAssistantTool.cs | 21 +- .../SessionMessageAssistantToolProvider.cs | 31 - .../Models/SessionMessageAssistantToolTime.cs | 7 - .../Models/SessionMessageCompaction.cs | 49 +- .../SessionMessageCompactionCompleted.cs | 61 + ...SessionMessageCompactionCompletedReason.cs | 22 + .../SessionMessageCompactionCompletedTime.cs | 16 + .../Models/SessionMessageCompactionFailed.cs | 55 + ...> SessionMessageCompactionFailedReason.cs} | 6 +- .../SessionMessageCompactionFailedTime.cs | 16 + .../Models/SessionMessageCompactionRunning.cs | 61 + .../SessionMessageCompactionRunningReason.cs | 22 + .../SessionMessageCompactionRunningTime.cs | 16 + src/OpenCode.Sdk/Models/SessionMessageInfo.cs | 18 + .../Models/SessionMessageLocationSwitched.cs | 64 + .../SessionMessageLocationSwitchedPrevious.cs | 30 + .../SessionMessageLocationSwitchedTime.cs | 16 + ...ched.cs => SessionMessageModelSelected.cs} | 13 +- ....cs => SessionMessageModelSelectedTime.cs} | 4 +- .../Models/SessionMessageShell.cs | 24 +- .../Models/SessionMessageShellOutput.cs | 34 + .../Models/SessionMessageShellStatus.cs | 32 + .../Models/SessionMessageSkill.cs | 55 + ...tionTime.cs => SessionMessageSkillTime.cs} | 4 +- .../Models/SessionMessageSynthetic.cs | 15 +- .../Models/SessionMessageSystem.cs | 9 +- .../SessionMessageToolStateCompleted.cs | 37 +- .../Models/SessionMessageToolStateError.cs | 42 +- .../Models/SessionMessageToolStateRunning.cs | 20 +- ...cs => SessionMessageToolStateStreaming.cs} | 6 +- src/OpenCode.Sdk/Models/SessionMessageUser.cs | 8 +- ...orUnknown.cs => SessionStructuredError.cs} | 11 +- ...geAssistantTokens.cs => TokenUsageInfo.cs} | 6 +- ...tTokensCache.cs => TokenUsageInfoCache.cs} | 4 +- .../{SessionMessage.cs => ToolContent.cs} | 6 +- src/OpenCode.Sdk/Models/ToolFileContent.cs | 2 +- src/OpenCode.Sdk/Models/ToolTextContent.cs | 2 +- .../Models/UnknownOpenCodeError.cs | 1 + ...lContent.cs => UnknownPromptFileSource.cs} | 7 +- .../UnknownSessionMessageAssistantContent.cs | 1 + .../Models/UnknownSessionMessageCompaction.cs | 39 + .../Models/UnknownSessionMessageInfo.cs | 33 + .../Models/UnknownSessionMessageToolState.cs | 1 + ...essionMessage.cs => UnknownToolContent.cs} | 7 +- .../known-assistant-content.json | 2 +- .../known-compaction-message.json | 1 + .../Serialization/known-tool-state.json | 2 +- .../unknown-compaction-status.json | 1 + .../Serialization/OpenCodeJsonContextTests.cs | 81 +- .../Support/GeneratedJsonSerializer.cs | 2 +- .../Generator/Binding/SpecBinderTests.cs | 79 +- .../Ingestion/OperationProjectorTests.cs | 24 +- .../Ingestion/PinnedSpecSmokeTests.cs | 52 +- .../Ingestion/PrefixItemsAdapterTests.cs | 56 +- .../Ingestion/PrefixItemsTripwireTests.cs | 7 +- .../Ingestion/SchemaProjectorTests.cs | 201 +- .../Output/GenerationWriterTests.cs | 2 +- ...erant_Marked_Union_Converters.verified.txt | 1 + .../Support/GenerationTestData.cs | 2 +- .../OpenCode.Sdk.Tools.Tests/ToolAppTests.cs | 3 +- .../Commands/GenerateCommand.cs | 3 +- .../Binding/Models/PendingOperationPlan.cs | 4 - .../Binding/Models/UnionFixedMarkerPlan.cs | 14 + .../Generator/Binding/Models/UnionPlan.cs | 6 + .../Binding/Models/UnionVariantPlan.cs | 3 + .../Generator/Binding/SchemaNameResolver.cs | 22 +- .../Generator/Binding/SchemaPlanBinder.cs | 183 +- .../Generator/Binding/SpecBinder.cs | 4 +- .../Generator/Emission/EmissionSyntax.cs | 4 +- .../Generator/Emission/ModelEmitter.cs | 33 +- .../Generator/Emission/TypeSyntaxEmitter.cs | 1 + .../Generator/Emission/UnionEmitter.cs | 58 +- .../Generator/GenerationCoordinator.cs | 27 +- .../Generator/GenerationReport.cs | 12 +- .../Generator/Ingestion/Models/LiteralKind.cs | 3 + .../Generator/Ingestion/Models/LiteralNode.cs | 2 +- .../Ingestion/Models/SpecOperation.cs | 5 +- .../Generator/Ingestion/Models/SpecSurface.cs | 11 - .../Ingestion/Projection/LiteralClassifier.cs | 29 +- .../Ingestion/Projection/OperationIdentity.cs | 4 +- .../Projection/OperationIdentityParser.cs | 16 +- .../Projection/OperationProjector.cs | 1 - .../Projection/PrefixItemsAdapter.cs | 45 +- .../Ingestion/Projection/SchemaProjector.cs | 15 +- .../Projection/SchemaShapeClassifier.cs | 53 + .../Ingestion/Projection/UnionNormalizer.cs | 67 +- .../Ingestion/Walls/SchemaWallPolicy.cs | 12 +- .../Output/GenerationWriter.cs | 2 +- tools/curation.json | 4 +- 119 files changed, 18264 insertions(+), 28511 deletions(-) create mode 100644 src/OpenCode.Sdk/Internal/Serialization/PromptFileSourceJsonConverter.cs create mode 100644 src/OpenCode.Sdk/Internal/Serialization/SessionMessageCompactionJsonConverter.cs rename src/OpenCode.Sdk/Internal/Serialization/{SessionMessageJsonConverter.cs => SessionMessageInfoJsonConverter.cs} (62%) rename src/OpenCode.Sdk/Internal/Serialization/{LlmToolContentJsonConverter.cs => ToolContentJsonConverter.cs} (69%) create mode 100644 src/OpenCode.Sdk/Models/LocationRef.cs rename src/OpenCode.Sdk/Models/{LlmToolContent.cs => PromptFileSource.cs} (76%) create mode 100644 src/OpenCode.Sdk/Models/PromptFileSourceInline.cs create mode 100644 src/OpenCode.Sdk/Models/PromptFileSourceUri.cs rename src/OpenCode.Sdk/Models/{PromptSource.cs => PromptMention.cs} (90%) create mode 100644 src/OpenCode.Sdk/Models/PromptSkillAttachment.cs create mode 100644 src/OpenCode.Sdk/Models/ServiceHealth.cs rename src/OpenCode.Sdk/Models/{SessionMessageAgentSwitched.cs => SessionMessageAgentSelected.cs} (77%) rename src/OpenCode.Sdk/Models/{SessionMessageModelSwitchedTime.cs => SessionMessageAgentSelectedTime.cs} (78%) create mode 100644 src/OpenCode.Sdk/Models/SessionMessageAssistantFinish.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageAssistantRetry.cs delete mode 100644 src/OpenCode.Sdk/Models/SessionMessageAssistantToolProvider.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageCompactionCompletedReason.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageCompactionCompletedTime.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageCompactionFailed.cs rename src/OpenCode.Sdk/Models/{SessionMessageCompactionReason.cs => SessionMessageCompactionFailedReason.cs} (80%) create mode 100644 src/OpenCode.Sdk/Models/SessionMessageCompactionFailedTime.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageCompactionRunning.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageCompactionRunningReason.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageCompactionRunningTime.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageInfo.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageLocationSwitched.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageLocationSwitchedPrevious.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageLocationSwitchedTime.cs rename src/OpenCode.Sdk/Models/{SessionMessageModelSwitched.cs => SessionMessageModelSelected.cs} (77%) rename src/OpenCode.Sdk/Models/{SessionMessageAgentSwitchedTime.cs => SessionMessageModelSelectedTime.cs} (78%) create mode 100644 src/OpenCode.Sdk/Models/SessionMessageShellOutput.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageShellStatus.cs create mode 100644 src/OpenCode.Sdk/Models/SessionMessageSkill.cs rename src/OpenCode.Sdk/Models/{SessionMessageCompactionTime.cs => SessionMessageSkillTime.cs} (79%) rename src/OpenCode.Sdk/Models/{SessionMessageToolStatePending.cs => SessionMessageToolStateStreaming.cs} (72%) rename src/OpenCode.Sdk/Models/{SessionErrorUnknown.cs => SessionStructuredError.cs} (64%) rename src/OpenCode.Sdk/Models/{SessionMessageAssistantTokens.cs => TokenUsageInfo.cs} (81%) rename src/OpenCode.Sdk/Models/{SessionMessageAssistantTokensCache.cs => TokenUsageInfoCache.cs} (81%) rename src/OpenCode.Sdk/Models/{SessionMessage.cs => ToolContent.cs} (77%) rename src/OpenCode.Sdk/Models/{UnknownLlmToolContent.cs => UnknownPromptFileSource.cs} (79%) create mode 100644 src/OpenCode.Sdk/Models/UnknownSessionMessageCompaction.cs create mode 100644 src/OpenCode.Sdk/Models/UnknownSessionMessageInfo.cs rename src/OpenCode.Sdk/Models/{UnknownSessionMessage.cs => UnknownToolContent.cs} (80%) create mode 100644 tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-compaction-message.json create mode 100644 tests/OpenCode.Sdk.Tests/Fixtures/Serialization/unknown-compaction-status.json create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/UnionFixedMarkerPlan.cs delete mode 100644 tools/OpenCode.Sdk.Tools/Generator/Ingestion/Models/SpecSurface.cs diff --git a/AGENTS.md b/AGENTS.md index 07efd397..48cc6968 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ direction and rationale, not law; only this list and the ADRs bind. 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); the 1.x legacy surface is never built (ADR-0005). + 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; diff --git a/CONTEXT.md b/CONTEXT.md index 9e154141..5ba8e767 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -59,13 +59,10 @@ A pseudo-terminal session managed through the API. ### This project's language -**Protocol surface** (historically "modern surface"): -The `v2.*`-prefixed protocol operation block — the only surface this SDK generates -(ADR-0005); public names carry no prefix. -_Avoid_: v2, V2 (in public naming) - -**Legacy surface**: -The un-prefixed operation block of the 1.x spec; never built (ADR-0005). +**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/docs/research/15-opencode-v2-platform.md b/docs/research/15-opencode-v2-platform.md index 6b072092..2f5e1f6a 100644 --- a/docs/research/15-opencode-v2-platform.md +++ b/docs/research/15-opencode-v2-platform.md @@ -116,7 +116,8 @@ mangling rule covers them. Two-day drift against session 12's census of the same — 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`; no `x-effect-stream` marker remains. The v1 durable +`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 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 index c54332ff..03c8f7c1 100644 --- 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 @@ -44,26 +44,26 @@ names, or concrete client names. Generated methods delegate once into a hand-wri Executes the 2026-08-13 direction (ADR-0005 revised: v2 protocol surface only; platform evidence: research doc 15). -- [ ] Pin a `v2`-branch snapshot of `packages/protocol/openapi.json` as `spec/openapi.json` +- [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). -- [ ] Add one ingestion-wall admit rule: a single-element `allOf` wrapper carrying only +- [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. - [ ] 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 are re-read from the v2 responses — do not assume the 1.x tables. -- [ ] Map dotted schema names mechanically (`Session.Message.Info` → `SessionMessageInfo`; +- [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). -- [ ] Adopt the v2 `location[...]` query convention as the only per-request scoping +- [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). -- [ ] Retire the Modern/Legacy surface split (`SpecSurface`, pending-legacy reporting, +- [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. -- [ ] Regenerate the selected model closure; update curation rows, serialization tests, and +- [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 @@ -71,10 +71,10 @@ evidence: research doc 15). **Areas:** `Generator/Binding/`, `ModelEmitter`, `tools/curation.json`, Binder/emitter tests, and committed generated models. -- [ ] Change `CSharpNamePolicy` to ordinary PascalCase acronym handling and regenerate the +- [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. -- [ ] Route optional non-null collection copying through an internal helper whose input is +- [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. - [ ] Add `handleParameter` to group curation. Require it and `handleName` together, @@ -103,8 +103,9 @@ and committed generated models. - [ ] Implement endpoint validation/joining and owned-versus-injected `HttpClient` lifetime. Disposing the root makes its wrappers unusable and never disposes a BYO client. - [ ] Resolve explicit password before one-time `OPENCODE_SERVER_PASSWORD` fallback. Decorate - each request, never default headers, with Basic user `opencode`, per-call-over-client - `x-opencode-directory`, and `OpenCode.Sdk/` User-Agent. + 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. - [ ] Use only `HttpClient.SendAsync(request, ResponseHeadersRead, cancellationToken)`. Own each request and buffered response, preserve cancellation, and wrap network/protocol failures as `OpenCodeTransportException`. diff --git a/spec/SNAPSHOT.md b/spec/SNAPSHOT.md index 31c6037a..cf497e93 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 afe14bb6..4d175c8a 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 8a4f7b6d..1f6c7e8f 100644 --- a/src/OpenCode.Sdk/.generated-manifest.json +++ b/src/OpenCode.Sdk/.generated-manifest.json @@ -1,43 +1,62 @@ { "files": [ - "Internal/Serialization/LlmToolContentJsonConverter.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", @@ -45,18 +64,24 @@ "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" ] } diff --git a/src/OpenCode.Sdk/.generation-incomplete b/src/OpenCode.Sdk/.generation-incomplete index c1434933..a97248a4 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/Internal/Serialization/OpenCodeJsonContext.cs b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs index 2d9e8576..560f5924 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/PromptFileSourceJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/PromptFileSourceJsonConverter.cs new file mode 100644 index 00000000..c9c04356 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Serialization/PromptFileSourceJsonConverter.cs @@ -0,0 +1,68 @@ +// 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() ?? throw new JsonException("The 'type' marker cannot be null."); + 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/SessionMessageCompactionJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageCompactionJsonConverter.cs new file mode 100644 index 00000000..d0031f72 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageCompactionJsonConverter.cs @@ -0,0 +1,69 @@ +// 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() ?? throw new JsonException("The 'status' marker cannot be null."); + 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 62% rename from src/OpenCode.Sdk/Internal/Serialization/SessionMessageJsonConverter.cs rename to src/OpenCode.Sdk/Internal/Serialization/SessionMessageInfoJsonConverter.cs index 68c5f841..d8b56241 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) @@ -50,25 +52,31 @@ public override SessionMessage Read(ref Utf8JsonReader reader, Type typeToConver var marker = markerElement.GetString() ?? throw new JsonException("The 'type' marker cannot be null."); 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 a7c3431c..5ecfdd1a 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; diff --git a/src/OpenCode.Sdk/Internal/Serialization/LlmToolContentJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/ToolContentJsonConverter.cs similarity index 69% rename from src/OpenCode.Sdk/Internal/Serialization/LlmToolContentJsonConverter.cs rename to src/OpenCode.Sdk/Internal/Serialization/ToolContentJsonConverter.cs index 0927249d..ca840236 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) @@ -44,25 +44,25 @@ public override LlmToolContent Read(ref Utf8JsonReader reader, Type typeToConver var marker = markerElement.GetString() ?? throw new JsonException("The 'type' marker cannot be null."); 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/Models/LocationRef.cs b/src/OpenCode.Sdk/Models/LocationRef.cs new file mode 100644 index 00000000..3b753731 --- /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/PromptAgentAttachment.cs b/src/OpenCode.Sdk/Models/PromptAgentAttachment.cs index f56e3923..7541d366 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 f17bd341..1bc0abf6 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 7be23e7b..acd5526b 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 00000000..30ecaa85 --- /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 00000000..dfb1816e --- /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 d1998bcc..d2e13861 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 00000000..d8eaf56d --- /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 00000000..8acc98c4 --- /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 77% rename from src/OpenCode.Sdk/Models/SessionMessageAgentSwitched.cs rename to src/OpenCode.Sdk/Models/SessionMessageAgentSelected.cs index dce79b25..e96712c2 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAgentSwitched.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAgentSelected.cs @@ -7,9 +7,9 @@ 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. @@ -27,7 +27,7 @@ public sealed record SessionMessageAgentSwitched : SessionMessage /// Gets the time value. /// [JsonPropertyName("time")] - public required SessionMessageAgentSwitchedTime Time { get; init; } + public required SessionMessageAgentSelectedTime Time { get; init; } /// /// Gets the type value. @@ -40,4 +40,11 @@ public sealed record SessionMessageAgentSwitched : SessionMessage /// [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 d01ce534..1ecc6394 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 6262c40c..71b81f85 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistant.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistant.cs @@ -9,7 +9,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a session message assistant value. /// -public sealed record SessionMessageAssistant : SessionMessage +public sealed record SessionMessageAssistant : SessionMessageInfo { /// /// Gets the id value. @@ -73,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. @@ -87,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 00000000..be884f91 --- /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 d829f102..381c16df 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageAssistantReasoning.cs @@ -17,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. /// @@ -30,10 +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 => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary>(input.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 00000000..0baa9215 --- /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/SessionMessageAssistantText.cs b/src/OpenCode.Sdk/Models/SessionMessageAssistantText.cs index 4124f6e5..ff4bcb93 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 dd792369..eb63d0b2 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; /// @@ -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 a9bd895e..00000000 --- a/src/OpenCode.Sdk/Models/SessionMessageAssistantToolProvider.cs +++ /dev/null @@ -1,31 +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; -using OpenCode.Sdk.Internal.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 => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary>(input.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 => field = OptionalCollectionInput.Normalize(value, field, static input => new ReadOnlyDictionary>(input.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 12a3894e..966de623 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 ea456dcc..58bdeb5c 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageCompaction.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageCompaction.cs @@ -1,55 +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 => 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 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 00000000..1b60f758 --- /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 00000000..c35159b2 --- /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 00000000..a7ff48c9 --- /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 00000000..1d92e93c --- /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 e5b0238e..78fa17a9 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 00000000..c044c1c3 --- /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 00000000..5f5c9139 --- /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 00000000..d214ed1f --- /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 00000000..81680c56 --- /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 00000000..7a21a455 --- /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 00000000..368dd383 --- /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 00000000..c5041889 --- /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 00000000..9d72d720 --- /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 77% rename from src/OpenCode.Sdk/Models/SessionMessageModelSwitched.cs rename to src/OpenCode.Sdk/Models/SessionMessageModelSelected.cs index 949da287..72e598d8 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageModelSwitched.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageModelSelected.cs @@ -7,9 +7,9 @@ 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. @@ -27,7 +27,7 @@ public sealed record SessionMessageModelSwitched : SessionMessage /// Gets the time value. /// [JsonPropertyName("time")] - public required SessionMessageModelSwitchedTime Time { get; init; } + public required SessionMessageModelSelectedTime Time { get; init; } /// /// Gets the type value. @@ -40,4 +40,11 @@ public sealed record SessionMessageModelSwitched : SessionMessage /// [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 0d4f7dd1..ae0f6536 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 b243331a..ba204ead 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageShell.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageShell.cs @@ -9,7 +9,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a session message shell value. /// -public sealed record SessionMessageShell : SessionMessage +public sealed record SessionMessageShell : SessionMessageInfo { /// /// Gets the id value. @@ -36,10 +36,10 @@ public sealed record SessionMessageShell : SessionMessage 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. @@ -47,9 +47,23 @@ public sealed record SessionMessageShell : SessionMessage [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 00000000..1ba1b4f3 --- /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 00000000..0db35d57 --- /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 00000000..c16b1084 --- /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 e4ee8680..24d56c38 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 069d09fc..0a5157ff 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageSynthetic.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageSynthetic.cs @@ -9,7 +9,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a session message synthetic value. /// -public sealed record SessionMessageSynthetic : SessionMessage +public sealed record SessionMessageSynthetic : SessionMessageInfo { /// /// Gets the id value. @@ -29,18 +29,19 @@ public sealed record SessionMessageSynthetic : SessionMessage [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 a8035652..9a8a53cf 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageSystem.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageSystem.cs @@ -9,7 +9,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a session message system value. /// -public sealed record SessionMessageSystem : SessionMessage +public sealed record SessionMessageSystem : SessionMessageInfo { /// /// Gets the id value. @@ -40,4 +40,11 @@ public sealed record SessionMessageSystem : SessionMessage /// [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 4cef18dc..2de1a1ff 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageToolStateCompleted.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageToolStateCompleted.cs @@ -31,50 +31,23 @@ public required IReadOnlyDictionary Input } } = new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal)); - /// - /// Gets the attachments value. - /// - [JsonPropertyName("attachments")] - public IReadOnlyList Attachments { get; init => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).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 => field = OptionalCollectionInput.Normalize(value, field, static input => new List(input).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 4500937a..a155107d 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 983eb02f..35138add 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 6c89ec19..2336a8f5 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 419618ce..8636a7ef 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageUser.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageUser.cs @@ -9,7 +9,7 @@ namespace OpenCode.Sdk.Models; /// /// Represents a session message user value. /// -public sealed record SessionMessageUser : SessionMessage +public sealed record SessionMessageUser : SessionMessageInfo { /// /// Gets the id value. @@ -47,6 +47,12 @@ public sealed record SessionMessageUser : SessionMessage [JsonPropertyName("agents")] 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/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 ff316f61..fa9a32ae 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 f324769e..2dc6f638 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 078ed48a..21b1e1a8 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 256ca83b..6c679353 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 698ab967..cf65dbe6 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 0e285c2a..4ed67bbe 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 fa84ba41..130eb3c1 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/UnknownLlmToolContent.cs b/src/OpenCode.Sdk/Models/UnknownPromptFileSource.cs similarity index 79% rename from src/OpenCode.Sdk/Models/UnknownLlmToolContent.cs rename to src/OpenCode.Sdk/Models/UnknownPromptFileSource.cs index 58b81984..adfee473 100644 --- a/src/OpenCode.Sdk/Models/UnknownLlmToolContent.cs +++ b/src/OpenCode.Sdk/Models/UnknownPromptFileSource.cs @@ -5,16 +5,16 @@ namespace OpenCode.Sdk.Models; /// -/// Preserves an unknown llm tool content payload. +/// Preserves an unknown prompt file source payload. /// -public sealed record UnknownLlmToolContent : LlmToolContent +public sealed record UnknownPromptFileSource : PromptFileSource { private readonly string _marker; /// /// Initializes an unknown union value from its marker and raw payload. /// [JsonConstructor] - public UnknownLlmToolContent(string type, JsonElement payload) + public UnknownPromptFileSource(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/Models/UnknownSessionMessageAssistantContent.cs b/src/OpenCode.Sdk/Models/UnknownSessionMessageAssistantContent.cs index c903cb1a..143c2b23 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 00000000..96268f85 --- /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 00000000..c77f4642 --- /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 547ff367..7c1eac2f 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/UnknownSessionMessage.cs b/src/OpenCode.Sdk/Models/UnknownToolContent.cs similarity index 80% rename from src/OpenCode.Sdk/Models/UnknownSessionMessage.cs rename to src/OpenCode.Sdk/Models/UnknownToolContent.cs index 464f80cb..e961cbf6 100644 --- a/src/OpenCode.Sdk/Models/UnknownSessionMessage.cs +++ b/src/OpenCode.Sdk/Models/UnknownToolContent.cs @@ -5,16 +5,16 @@ namespace OpenCode.Sdk.Models; /// -/// Preserves an unknown session message payload. +/// Preserves an unknown tool content payload. /// -public sealed record UnknownSessionMessage : SessionMessage +public sealed record UnknownToolContent : ToolContent { private readonly string _marker; /// /// Initializes an unknown union value from its marker and raw payload. /// [JsonConstructor] - public UnknownSessionMessage(string type, JsonElement payload) + public UnknownToolContent(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/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-assistant-content.json b/tests/OpenCode.Sdk.Tests/Fixtures/Serialization/known-assistant-content.json index b9f4ec4a..e01795fd 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 00000000..99906fac --- /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 c3fd5c77..b61c9af0 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 00000000..ca6dd512 --- /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/Serialization/OpenCodeJsonContextTests.cs b/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs index 0f859fa5..bd0bc925 100644 --- a/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs +++ b/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs @@ -14,13 +14,13 @@ 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.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"); } @@ -30,13 +30,14 @@ public async Task Deserialize_Should_Normalize_Absent_Optional_Nonnull_Collectio { 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.Metadata).IsEmpty(); await Assert.That(user.Files).IsEmpty(); await Assert.That(user.Agents).IsEmpty(); + await Assert.That(user.Skills).IsEmpty(); } [Test] @@ -44,12 +45,45 @@ public async Task Deserialize_Should_Preserve_Unknown_Outer_Variant_Byte_For_Byt { 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); } @@ -90,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] @@ -135,7 +169,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(); } @@ -149,35 +183,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/Support/GeneratedJsonSerializer.cs b/tests/OpenCode.Sdk.Tests/Support/GeneratedJsonSerializer.cs index b7945917..c2a90ba6 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.Tools.Tests/Generator/Binding/SpecBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs index 4d96acf4..2cc37146 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs @@ -25,12 +25,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 +39,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) @@ -178,6 +186,61 @@ 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_Require_Curation_For_Every_Selected_Group() { diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/OperationProjectorTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/OperationProjectorTests.cs index e796c029..0fde1ce9 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 dbcb08c8..93263a47 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 9986e3e5..d55f681a 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 7fa4eff9..aa5f8bce 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 537a8dba..5e69fae0 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/Output/GenerationWriterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Output/GenerationWriterTests.cs index a32711e1..7c75abff 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Output/GenerationWriterTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Output/GenerationWriterTests.cs @@ -198,7 +198,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/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 7977bccd..01929d86 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. diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs index cac1866f..70440afb 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs @@ -35,7 +35,7 @@ public static MockFileSystem CreateCommandFileSystem() { var spec = new SpecDocumentBuilder() .WithOperation("v2.health.get", path: "/api/health") - .WithOperation("session.list", path: "/session") + .WithOperation("v2.session.list", path: "/api/session") .BuildJson(); var fileSystem = CreateFileSystem(); fileSystem.Initialize().With( diff --git a/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs b/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs index 02919293..35bab9c1 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] diff --git a/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs b/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs index d5e372af..86f4af10 100644 --- a/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs +++ b/tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs @@ -56,8 +56,7 @@ protected override async Task ExecuteAsync(CommandContext context, Settings private void WriteSummary(GenerationReport report) { _console.MarkupLine($"[grey]Selected operations:[/] {report.SelectedOperationIds.Count.ToString(CultureInfo.InvariantCulture)}"); - _console.MarkupLine($"[yellow]Pending modern operations:[/] {report.PendingModernOperationIds.Count.ToString(CultureInfo.InvariantCulture)}"); - _console.MarkupLine($"[yellow]Pending legacy operations:[/] {report.PendingLegacyOperationIds.Count.ToString(CultureInfo.InvariantCulture)}"); + _console.MarkupLine($"[yellow]Pending operations:[/] {report.PendingOperationIds.Count.ToString(CultureInfo.InvariantCulture)}"); } private void WritePaths(WriteResult result) diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PendingOperationPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/PendingOperationPlan.cs index 762a30aa..51e86f7d 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 00000000..eb01456a --- /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 97ff2ac8..5381892c 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 e2ae45e5..328e2785 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/SchemaNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs index b544d0a4..88d64456 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs @@ -92,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 0b9ce862..1821f1e0 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs @@ -82,9 +82,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) @@ -95,70 +96,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) { diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs index 61488a4f..d2b4662f 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs @@ -29,12 +29,10 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera 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(); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs index 74d130fa..a31c322f 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 @@ -70,7 +70,7 @@ 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); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/ModelEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/ModelEmitter.cs index 63d39ffc..42500ee3 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) { @@ -282,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."), }; @@ -348,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/TypeSyntaxEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/TypeSyntaxEmitter.cs index f340b0b8..024e77de 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 efaf6e01..4dcfea3e 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}'."), }; @@ -396,6 +419,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 +483,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 c97a0fe6..b7b0ec2e 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; @@ -32,21 +31,13 @@ public async Task GenerateAsync(GenerationRequest request, Can var plan = _binder.Bind(document, selection, curation); - var modern = plan + var pending = 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) - .Select(static operation => operation.OperationId) - .Order(StringComparer.Ordinal) - .ToArray(); - - var marker = modern.Length + legacy.Length > 0 ? CreatePartialMarker(plan.SelectedOperationIds, modern, legacy) : null; + var marker = pending.Length > 0 ? CreatePartialMarker(plan.SelectedOperationIds, pending) : null; var writeResult = await _writer .WriteAsync( @@ -61,25 +52,21 @@ public async Task GenerateAsync(GenerationRequest request, Can 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 091a74f5..fe0ca0c2 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 35561f7a..3fd26bb8 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 921ebc1b..69c2246d 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 36a97943..38ffcae2 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 d522ecb8..00000000 --- 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/LiteralClassifier.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs index 11098345..7dc76f5e 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 b55ee7db..f39a258f 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 e2fab263..3b46f76e 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 f096645e..c4dc2404 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 540efb8f..5aa5bf5e 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 aea840cd..e468dbb3 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 61f35e9c..3b467348 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/Walls/SchemaWallPolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Walls/SchemaWallPolicy.cs index 6fde243d..b9db45a5 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 99d36b7c..6c678e51 100644 --- a/tools/OpenCode.Sdk.Tools/Output/GenerationWriter.cs +++ b/tools/OpenCode.Sdk.Tools/Output/GenerationWriter.cs @@ -161,7 +161,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), diff --git a/tools/curation.json b/tools/curation.json index 377bf301..7bc92ebc 100644 --- a/tools/curation.json +++ b/tools/curation.json @@ -14,13 +14,13 @@ }, "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." From 784763b529d2ed6226fc9f9e7d559d8c028c13ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 15:16:11 +0300 Subject: [PATCH 06/28] docs(agents): hand over the post-retarget Arc B state Replace the consumed pre-retarget handoff with a comprehensive onboarding handoff (reading order, Task 0 machinery, v2 wire contracts, Tasks 1-4 detail, demo procedure, stop conditions); refresh the ROADMAP status to the executed retarget. --- docs/ROADMAP.md | 9 +- .../handover-prompts/HANDOFF-2026-08-13-2.md | 233 +++++++ .../handover-prompts/HANDOFF-2026-08-13.md | 608 ------------------ 3 files changed, 238 insertions(+), 612 deletions(-) create mode 100644 docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md delete mode 100644 docs/agents/handover-prompts/HANDOFF-2026-08-13.md diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index de8e70a6..1fecee73 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -13,10 +13,11 @@ binds their reachable closure, emits committed Roslyn-built models plus source-g metadata, and owns deterministic `generate` / `generate --verify` writes through a guarded manifest. The partial-operation marker blocks every package while breadth remains incomplete. -Direction sealed 2026-08-13: the SDK targets the **v2 protocol surface only** (ADR-0005; -platform evidence: research doc 15). `spec/` still holds the 1.18.15 pin until the retarget -task at the head of M1 Arc B executes. The active work is M1 Arc B: the retarget plus the -minimal transport and callable client with typed errors and `NoThrow`; see the just-in-time +Direction sealed and executed 2026-08-13: the SDK targets the **v2 protocol surface only** +(ADR-0005; platform evidence: research doc 15), and `spec/` pins v2-branch commit `a6a712a` +(Arc B Task 0 — landed). The active work is the rest of M1 Arc B: operation binding, the +minimal transport core, and the callable client with typed errors and `NoThrow`; see the +just-in-time [M1 Arc B callable-client plan](superpowers/plans/2026-08-13-m1-arc-b-callable-client.md). ## Milestones diff --git a/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md b/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md new file mode 100644 index 00000000..41f8c511 --- /dev/null +++ b/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md @@ -0,0 +1,233 @@ +# M1 Arc B Handoff — Post-Retarget (Tasks 1–4) + +Date: 2026-08-13 + +Consume this document against live Git and delete it when Arc B ships. It records unfinished +session state, not evergreen architecture. It supersedes `HANDOFF-2026-08-13.md` (deleted in +the same commit; its Task 1 model-policy work and the wire-contract re-derivation it planned +have landed). + +## Onboarding Reading Order + +1. `AGENTS.md` — locked decisions. Note: ADR-0005 was **revised in place** on 2026-08-13; the + SDK now targets the opencode v2 protocol surface only. There is no legacy surface, no + dual-surface machinery, and no v1 convention debt anywhere. +2. `docs/adr/0005-v2-protocol-surface-only.md` (the revision), plus ADRs 0003/0004/0007/0008/ + 0009 — the binding design spine. +3. `docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md` — the canonical execution + plan. Task 0 and the two model-policy items of Task 1 are checked off; everything unchecked + is this handoff's work. +4. `docs/research/15-opencode-v2-platform.md` — the canonical picture of the v2 platform + (branch, architecture, distribution, dialect, live verification transcript). +5. Research log Session 17 (`docs/research/00-research-log.md`) — the retarget decision chain. +6. `docs/engineering/coding-style.md` and `docs/engineering/testing-style.md` — before writing + any code. + +Historical context (read when needed, never as current fact): research docs 09/10 are the +dated v1-line genealogy; the three `docs/superpowers/specs/` documents are vision/reference +with a banner marking their v1 dual-surface facts historical; sessions 12/13 in the research +log record the ingestion redesign the current `Generator/Ingestion` implements. + +## Git And Workspace State + +| Item | State | +|---|---| +| Workspace | `/home/deniz/src/opencode-sdk-dotnet` | +| Branch | `feature/m1-callable-client` | +| HEAD | `911b7f3 feat(generator): retarget the pin to the opencode v2 protocol surface` | +| Ahead of `origin/master` (`3e3e058`) | 5 commits, no remote branch, no pushes | +| Worktree | Clean | +| Gates at HEAD | generate ✓, `generate --verify` current ✓, Release build 0W/0E ✓, tools tests 221 ✓, SDK tests 45 ✓ (net8/9/10), format ✓, Slopwatch 0 ✓ | + +The maintainer authorized implementation commits inside this development loop; pushing still +requires an explicit request. SSH operations here may need `GIT_SSH_COMMAND='ssh -F /dev/null'`. + +The five commits: `f35f741` (Polyfill research), `7f317f0` (Arc B plan), `d0865e9` (model +policy: PascalCase acronyms + `OptionalCollectionInput`, plus the retarget decision's docs +pass), `5f71294` (sealed conventions: no taste curation, v1 ledger closed, surface-split +retirement), `911b7f3` (Task 0: the retarget itself). + +## The Pin And What Task 0 Built + +`spec/openapi.json` is `packages/protocol/openapi.json` at `anomalyco/opencode` v2-branch +commit `a6a712a3ac72248c9b2f2f883e752e6e18ef8c40` (provenance: `spec/SNAPSHOT.md`). 104 paths / +120 operations / 324 schemas, all `/api`-mounted, all operationIds `v2.`-prefixed. `generate` +reports: Selected 2, Pending 118. Refreshes are deliberate snapshot moves (M2 boundary), never +HEAD-tracking. + +Task 0 changes an incoming agent must know before touching the generator: + +- **Ingestion-wall admits** (each fail-closed to the observed population, all red-tested): + a single-element validation-only `allOf` wrapper unwraps into its host + (`SchemaShapeClassifier.IsValidationOnlyAllOfWrapper`); a single-value number/integer enum + projects as `LiteralKind.Number`; a bare `{"type":"array"}` projects as array-of-any-value; + `prefixItems` admits exactly two shapes in `PrefixItemsAdapter` — the closed tuple + (`minItems == maxItems == arity`, no `items`) and Effect's NonEmptyArray encoding (single + prefix element sharing the `items` `$ref` target → plain `ArrayNode`); the old + location-pinned admission is retired, shape validation replaced it. +- **Union machinery for the v2 topology** (`SchemaPlanBinder`, `UnionNormalizer`, + `UnionEmitter`, `ModelEmitter`): the discriminating marker is *selected* (first property, in + the first branch's document order, that every branch carries with one kind and pairwise + distinct values — `status` beats the duplicated `type` on the Compaction family); a union + branch may be a **nested marked union** (exactly one level): `SessionMessageCompaction : + SessionMessageInfo` carries `BaseTypeName` + `FixedMarker` (`type=compaction`) on its + `UnionPlan`, its variants override both chain markers, its unknown carrier fixes the outer + tag; promoted inline branches are named root + marker value (`PromptFileSourceInline`/`Uri`); + the special-number rule accepts v2's one-level-nested encoding. +- **Two serialization bugs were caught by the new nested-union tests and fixed in the + emitter** — unknown-carrier marker overrides now emit `[JsonPropertyName]` (STJ otherwise + fails constructor binding), and a union converter's `Write` serializes a nested-union + variant through its **declared base type's** typed context property so the nested + converter's raw-payload path runs (runtime-type metadata would serialize the carrier as an + ordinary record). Both behaviors are covered by `OpenCode.Sdk.Tests` serialization tests. +- **Single surface**: `SpecSurface` is deleted; `OperationIdentityParser` refuses any + operationId without the `v2.` prefix (located error); `GenerationReport` carries one + `PendingOperationIds`; the `.generation-incomplete` marker and the CLI print one "Pending + operations" count. The word "legacy" does not exist in production code. +- **Writer semantics** (`GenerationWriter`): manifest-diff cleanup — files in the previous + manifest but absent from the new plan are deleted; the writer touches nothing outside the + manifest and its admitted subtrees (`Models/`, `Internal/Serialization/`). Task 3 broadens + ownership and must add the unmanifested-overwrite refusal and provenance-header checks. + +## Selected Wire Contracts (v2) + +| Fact | `v2.health.get` | `v2.session.message` | +|---|---|---| +| Route | GET `/api/health` | GET `/api/session/{sessionID}/message/{messageID}` | +| 200 | `$ref ServiceHealth` — `{healthy, version, pid}` (observed live) | `{data: Session.Message.Info}`, classified Data | +| Owner/method | `OpenCodeClient.GetHealthAsync` | `SessionClient.GetMessageAsync(messageId, …)` | +| Response | `HealthResponse` | `SessionMessageResponse.Message` | + +**The error maps are the one unchecked Task 0 box:** they have NOT been derived from the v2 +spec. Do not assume the 1.x tables (400/401/404 and the duplicated-404 union were v1 facts). +Read the v2 responses of both operations from the pin as the first step of operation binding. + +## Remaining Work (Arc B Tasks 1–4) + +The plan document is canonical; this is the operational detail on top of it. + +### Task 1 remainder — operation binding + +- `handleParameter` joins `GroupCuration` (`[JsonPropertyName("handleParameter")]`). + Validation: root placement rejects client/handle fields; client placement requires + `clientName`; `handleName` and `handleParameter` are paired; the handle parameter names a + required path parameter on at least one selected operation in that group; curation-to-spec + orphan checks stay global; selection-scoped coverage during staged generation. +- Consumed-only plan types (`ClientPlan`, `OperationPlan`, `OperationParameterPlan`, + `EnvelopePlan`, `ErrorMapPlan`) + `OperationPlanBinder`. Known seam: `SchemaPlanBinder` + still resolves type names internally (`TypePlanBinder` is a private nested class; + `SchemaBindingResult` discards the name map). Resolve names once per `SpecBinder.Bind`, pass + the same map to schema and operation binding; do not let operation binding reinterpret + schemas or read raw OpenAPI. +- Bind only ordinary GET + JSON + path-parameter operations with one 200 success, no + body/query/stream/wildcard. Handle-declared groups put the handle parameter on the handle; + everything else stays flat. Add an operation/protocol binding error category — do not + mislabel route/status/media failures as schema errors. +- Tests: complete pinned plans, handle-versus-method parameters, collisions, documentation, + status maps, batched failures, determinism; a synthetic same-shape operation with different + names binds without new production branches; an operation in a group without a handle + declaration stays flat even when it carries `sessionID`. +- Blueprint (recommended, consumed-only): expected final values `OpenCodeClient.GetHealthAsync` + → `HealthResponse` / `HealthResponseAdapter`; `SessionClient.GetMessageAsync` → + `SessionMessageResponse` / `SessionMessageResponseAdapter`. Route members enter the API + baseline — settle their shape before emission (reference shape: nested + `OpenCodeRoutes.Health.Get`, `OpenCodeRoutes.Sessions.GetMessage(sessionId, messageId)`; + nested public classes trip CA1034 — use the narrow arbitration pattern, never a global + weakening). + +### Task 2 — hand-written HTTP core + +Identity files per the plan (options/response/exception spine, `IEnvironmentProvider`, +`Pipeline`, `ResponseAdapter`, `SystemEnvironmentProvider`, friend metadata). Locked +decisions that survive the retarget verbatim: URI constructors own endpoint authority; BYO +`HttpClient` requires `options.Endpoint`; never touch `BaseAddress`/`DefaultRequestHeaders`; +absolute HTTP(S) endpoints without query/fragment, normalized path prefix preserved; owned +client disposed, injected never; root disposal marks wrappers unusable; single send primitive +`SendAsync(request, ResponseHeadersRead, ct)`; Basic auth user `opencode`, explicit password +before one-time `OPENCODE_SERVER_PASSWORD` fallback; per-request decoration, never shared +headers; `OpenCode.Sdk/` User-Agent (the safe informational-version +extraction still needs an explicit decision + tests — do not silently substitute assembly +version). **Changed by the retarget:** there is no `x-opencode-directory` header — per-request +scoping rides the v2 `location[...]` query convention, bound as operation parameters (neither +selected M1 operation takes one, so M1 ships no directory affordance at all). +Error channels: throw by default; `OpenCodeRequestOptions.NoThrow` per call suppresses only +API-status exceptions; unknown tag / undeclared status / known tag at wrong status → +`UnknownOpenCodeError` with exact tag and raw `JsonElement`; malformed non-2xx JSON → null +typed error + exact raw body; unexpected/malformed 2xx → `OpenCodeTransportException`; +`OperationCanceledException` always passes through. Pipeline signature: +`Task ExecuteAsync(HttpMethod, string route, ResponseAdapter, +OpenCodeRequestOptions?, CancellationToken) where TResponse : OpenCodeResponse` — buffer the +JSON body, synchronous adapter maps `(status, rawBody)`, pipeline applies throw-vs-NoThrow +after adaptation. + +### Task 3 — generated callable surface and ownership + +One file per public type (`OpenCodeClient`, `SessionsClient`, `SessionClient`, both +responses, `OpenCodeRoutes`; adapters under `Internal/ResponseAdapters/`); +`OperationMethodEmitter` returns members to `ClientEmitter` (no partial files); virtual +members + protected mock constructors (unoverridden member → instructive +`InvalidOperationException`, never NRE); guarded payload getters and `PrintMembers`; writer +ownership extension with unmanifested-overwrite refusal + provenance-header requirement + +case-insensitive collision checks; CS1591 gate on; Linux CI `generate --verify` step; +exception constructors need deliberate CA1032 treatment before the API baseline. + +### Task 4 — contracts and M1 closure + +Contract matrix through a recording `HttpMessageHandler` (health + message success/error +statuses under default and `NoThrow`; error skew: unknown tag, undeclared status, known tag at +wrong status, malformed body; envelope guards incl. `ToString`; route escaping; endpoint +joining; auth resolution; User-Agent token; cancellation; transport failures; owned/BYO +lifetime; mock seam). Pin `PublicApiGenerator` 11.5.4 + `Verify.TUnit`, review one API +baseline. Keep `.generation-incomplete`; pack must fail only because breadth is pending. +net472 runs on Windows CI as the ns2.0 proxy; `App.net472.config` never flows into the +package. + +## Real-Server Demonstration + +`opencode2` v0.0.0-next-17403 is installed on this machine +(`~/.nvm/versions/node/v24.18.0/bin/opencode2`, npm `@opencode-ai/cli@next`). Verified live +this session: install starts no server (`opencode2 service status` → stopped); +`opencode2 serve --hostname 127.0.0.1 --port

` prints the endpoint and a generated +password; unauthenticated `/api/health` → 401; Basic `opencode:` → 200 +`{"healthy":true,"version":"…","pid":…}`; `/api/session?limit=2` returns real sessions in the +`{data}` envelope. For the demo: start a password-enabled `opencode2 serve`, obtain IDs via +the pinned surface's session-list and message-list operations, call only the two generated +methods from a temporary `.scratchpad/` console, print the health payload + concrete message +type + message `Id`, paste commands and output into the PR, and record the version skew +between the pin (`a6a712a`) and the installed `next` build as visible demo evidence. + +## Final 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 must succeed; pack must fail only on the `.generation-incomplete` marker. + +## Stop Conditions + +Stop and involve the maintainer if: the Binder needs Microsoft.OpenApi or raw JSON for a fact +SpecIR claims to provide; an emitter needs an operation ID, wire group, or concrete +client-name branch; a selected shape exceeds the declared M1 capability rather than failing +closed; a **structural** union enters the selected closure (nested *marked* unions are now +supported — one level only); generated ownership cannot distinguish hand-written files; a +temporary public or transport API becomes necessary; any locked TFM fails to compile; partial +generation packs successfully; or retry/telemetry/DI/launcher/SSE/breadth becomes necessary +merely to call the two M1 operations. + +## Recommended First Actions + +1. Read the onboarding list above; inspect `git log origin/master..HEAD` and confirm the + worktree is clean. +2. Run the first six gates once to confirm the green baseline before changing anything. +3. Derive both operations' v2 error maps from the pin (the open Task 0 box) — this feeds + directly into `ErrorMapPlan`. +4. Red-test `handleParameter` and the two pinned operation plans, plus the synthetic + same-shape operation, before implementing `OperationPlanBinder`. +5. Continue in the plan's task order; delete this handoff when Arc B ships. diff --git a/docs/agents/handover-prompts/HANDOFF-2026-08-13.md b/docs/agents/handover-prompts/HANDOFF-2026-08-13.md deleted file mode 100644 index 5819eb62..00000000 --- a/docs/agents/handover-prompts/HANDOFF-2026-08-13.md +++ /dev/null @@ -1,608 +0,0 @@ -# M1 Arc B Callable Client Handoff - -Date: 2026-08-13 - -Consume this document against live Git and delete it when Arc B ships. It records unfinished -session state, not evergreen architecture. - -## Direction Update — v2 Retarget (sealed after this handoff was written) - -The SDK now targets the v2 protocol surface only (ADR-0005 as revised 2026-08-13; platform -evidence: `docs/research/15-opencode-v2-platform.md`). The canonical plan gained a Task 0 -that retargets the spec pin to a `v2`-branch snapshot before Task 1 continues. Consequences -for this handoff: the uncommitted Task 1 model-policy work remains valid and wanted (the -policy code and its tests are surface-independent; the 17 regenerated models regenerate -again from the v2 pin in Task 0); the "Selected Wire Contracts" section below describes the -retiring 1.18.15 pin — Task 0 re-derives both operations' contracts (the health success -becomes the `ServiceHealth` object); the real-server demonstration targets `opencode2 serve` -(npm `@opencode-ai/cli@next`, already installed on this machine). Sealed follow-ups -(2026-08-13, later): the v1 ledger is fully closed — no v1 convention survives, so the -`x-opencode-directory` decoration bullet below is void (v2 `location[...]` query is the only -scoping mechanism); schema names map mechanically (no taste curation — `Session.Message.Info` -becomes `SessionMessageInfo`); the Modern/Legacy surface split is retired in Task 0. - -## Objective - -Complete M1 Arc B: make `v2.health.get` and `v2.session.message` callable through their final -generated SDK surface, backed by a minimal hand-written HTTP core with typed errors and per-call -`NoThrow`, then demonstrate both calls against a real `opencode serve`. - -The canonical execution plan is -`docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md`. Read it before changing code. -The binding decisions are `AGENTS.md` and ADRs 0003, 0004, 0007, 0008, and 0009. The files under -`docs/superpowers/specs/` are reference material, not binding law. - -## Git And Workspace State - -| Item | State | -|---|---| -| Workspace | `/home/deniz/src/opencode-sdk-dotnet` | -| Branch | `feature/m1-callable-client` | -| HEAD | `7f317f0 docs(plan): define M1 callable client arc` | -| Branch base | `f35f741 docs(research): inventory Polyfill capabilities` | -| Remote base | `origin/master` at `3e3e058` | -| Ahead of `origin/master` | 2 commits | -| Remote feature branch | None | -| Pushes | None | -| Staging area | Empty | -| Worktree | Dirty: partial Task 1 model-policy implementation | - -The maintainer said `commit ve başla`, so implementation commits were authorized for this -development loop. Do not push without a new explicit request. SSH operations in this environment -may require `GIT_SSH_COMMAND='ssh -F /dev/null'`. - -Commit `7f317f0` is complete and should be retained. It adds the Arc B plan, updates the ROADMAP, -aligns ADR-0004/0007/0008, updates the research chain, removes the stale broad M1 plan, and -corrects affected reference documents. Before Task 1 code started, that docs state passed Release -build, 230 tests, format, Slopwatch, and `git diff --check`. - -## Current Uncommitted Work - -A delegated worker implemented the model-policy portion of Task 1. Nothing is staged or -committed. Do not run a blanket checkout/reset: inspect and preserve the intended work first. - -### Intended model-policy changes - -The intended change set does the following: - -- Changes `CSharpNamePolicy` from the old two-character-uppercase special case to ordinary - PascalCase: `id` -> `Id`, `sessionID` -> `SessionId`, `messageID` -> `MessageId`, - `callID` -> `CallId`, `URL` -> `Url`, and `APIError` -> `ApiError`. -- Adds focused casing tests in - `tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs`. -- Replaces direct `value is null` checks in non-null optional collection accessors with a - generated nullable-input helper, preserving a non-null public getter. -- Adds `OptionalCollectionInputEmitter` and generated - `src/OpenCode.Sdk/Internal/Serialization/OptionalCollectionInput.cs`. -- Updates `SourceEmitter` and `.generated-manifest.json` to own the helper. -- Adds an emitter snapshot and a focused assertion that optional non-null collections route - through the helper. -- Regenerates the selected model closure with `Id`-style members and expression-bodied init - accessors. -- Updates SDK serialization tests to prove absent optional collections become empty while the - existing explicit-null fixture still throws. - -The helper currently emits this shape: - -```csharp -internal static class OptionalCollectionInput -{ - internal static T Normalize(T? value, T empty, Func copy) - where T : class => value is null ? empty : copy(value); -} -``` - -Generated properties use the already initialized backing field as the empty value: - -```csharp -public IReadOnlyDictionary Metadata -{ - get; - init => field = OptionalCollectionInput.Normalize(value, field, static input => ...); -} = ...; -``` - -This is intentional. System.Text.Json's source-generated record path may pass null through the -init path for an absent optional collection. The helper's input is honestly nullable, while the -public property stays non-null. With `RespectNullableAnnotations=true`, explicit wire null is -rejected before assignment. Do not add `[AllowNull]`: a probe showed it also admits explicit JSON -null, violating the pinned schema. - -### Files belonging to the intended partial work - -| Area | Files | -|---|---| -| Naming | `CSharpNamePolicy.cs`, `CSharpNamePolicyTests.cs` | -| Collection emission | `ModelEmitter.cs`, `OptionalCollectionInputEmitter.cs`, `SourceEmitter.cs` | -| Emitter tests | `ModelEmitterTests.cs`, `OptionalCollectionInputEmitterTests.cs`, both affected snapshots | -| Product tests | `OpenCodeJsonContextTests.cs` | -| Generated output | `OptionalCollectionInput.cs`, `.generated-manifest.json`, 17 changed model files | - -The changed generated models include `MessageNotFoundError`, `ModelRef`, -`SessionMessageAgentSwitched`, `SessionMessageAssistant`, assistant content/tool/provider models, -`SessionMessageCompaction`, switched/shell/synthetic/system/user models, -`SessionMessageToolStateCompleted`, and `SessionNotFoundError`. - -### Formatter/analyzer collateral requiring review - -The dirty worktree also contains unrelated edits in these files: - -```text -tools/OpenCode.Sdk.Tools/Commands/GenerateCommand.cs -tools/OpenCode.Sdk.Tools/Generator/Binding/BindingErrorCollector.cs -tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs -tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs -tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs -tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs -tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs -tools/OpenCode.Sdk.Tools/Generator/GenerationCoordinator.cs -tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs -tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs -``` - -Most are formatting/local-flow changes. Two replace `GroupBy(...).Count() > 1` with -`Skip(1).Any()`, which is behaviorally equivalent and likely analyzer-driven. They are not needed -for the model-policy change. Review them individually; either discard them before the first code -commit or isolate them in a justified refactor. Do not accidentally mix them into operation -binding review. - -### Reported verification of the partial work - -The delegated worker reported: - -| Check | Result | -|---|---| -| Focused Tools tests | 12 passed | -| SDK serialization tests | 13 passed on each of net8.0, net9.0, net10.0 | -| `generate --verify` | Current | -| `dotnet build OpenCode.slnx --no-restore` | 0 warnings, 0 errors | -| `dotnet format OpenCode.slnx --no-restore --verify-no-changes` | Passed | -| Slopwatch | 0 issues | -| `git diff --check` | Passed | - -These were not converted into a commit. After cleaning the collateral, rerun the full repository -tests, not only the focused set, before committing. - -Scratch state is non-authoritative. `.scratchpad/Directory.Build.props` exists as the required -stub; `.scratchpad/stj-nullability-probe/` retains only ignored `bin/` and `obj/`; the source probe -was deleted. `.scratchpad/worktrees/` is empty. - -## What Has Not Started - -No operation/client binding code has been written. Specifically: - -- `tools/curation.json` has no `handleParameter` yet. -- `GroupCuration` has no matching property. -- `EmitPlan` still contains only selected IDs, models, unions, registry, and pending operations. -- There are no `ClientPlan`, `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, or - `ErrorMapPlan` types. -- There is no `OperationPlanBinder`. -- There are no client, route, envelope, operation-method, or response-adapter emitters. -- There is no hand-written pipeline, option/response/exception spine, or SDK friend assembly. -- `OpenCode.Sdk.csproj` has no net472 `System.Net.Http` framework reference. -- `GenerationWriter` still owns only `Models/` and `Internal/Serialization/`. -- `CS1591` is still disabled. -- `PublicApiGenerator` is not pinned or referenced. -- There is no generated `OpenCodeClient`, `SessionsClient`, `SessionClient`, response, or route. -- No real-server demonstration has run. - -## Locked And Approved Arc B Decisions - -### Generator boundary - -This is an opencode-specific generator, not a universal OpenAPI generator. It may fail closed on -opencode dialect shapes outside the current selected capability. However, endpoint and client -names must not be hard-coded in emitters or operation-ID branches. - -The intended flow is: - -```text -SpecIR + curation - -> Binder policy - -> final Client/Operation/Envelope/Error plans - -> dumb Roslyn emitters -``` - -Emitters must not know `v2.session.message`, `health`, `session`, or `SessionClient`. They render -final plan values. A synthetic operation with the same supported shape but different names must -bind and emit without a new production branch. - -### Bound handle - -`SessionsClient` is the collection facade. It will eventually carry collection operations such -as list/create and currently exposes `GetSessionClient`. - -`SessionClient` is a bound handle containing only the shared pipeline and immutable session ID. -It caches no server state. Session-scoped operations exist only on this handle; there is no -duplicate flat overload. - -OpenAPI cannot infer that `sessionID` should be partially applied. This is public API taste and -therefore curation. Update the modern `session` row to: - -```json -{ - "placement": "client", - "clientName": "Sessions", - "handleName": "SessionClient", - "handleParameter": "sessionID" -} -``` - -The Binder applies one general rule to any modern group declaring a handle. Legacy remains flat, -even if a legacy route contains `sessionID`. - -### Identifier casing - -Use ordinary PascalCase for all acronym tokens, regardless of length. Wire spelling remains in -`JsonPropertyName`. Brand spellings such as `OAuth` remain curated exceptions when reached. - -### Collection nullability - -Requiredness and nullability are independent. For an optional non-null collection: - -| Wire | SDK result | -|---|---| -| Property absent | Immutable empty collection | -| Property present with collection/object | Immutable defensive copy | -| Property present as explicit null | `JsonException` | - -Do not make the public property nullable merely because the deserializer's assignment path may -use null for absence. - -### HTTP and endpoint policy - -- Add `` conditionally for net472. Do not add the old - `System.Net.Http` NuGet package. Add the same framework reference to the net472 test path only - if test code directly needs it. -- Use `HttpClient.SendAsync(HttpRequestMessage, ResponseHeadersRead, CancellationToken)` as the - only send primitive. -- URI constructors make their URI authoritative. Reject a conflicting non-null - `OpenCodeClientOptions.Endpoint`. -- BYO `HttpClient` requires `options.Endpoint`. -- Never read or mutate `HttpClient.BaseAddress` or `DefaultRequestHeaders`. -- Accept only absolute HTTP(S) endpoint URIs without query or fragment; preserve a normalized - endpoint path prefix when joining generated routes. -- Self-created `HttpClient` is owned and disposed. Injected `HttpClient` is never disposed. -- Disposing the root still marks every SDK wrapper unusable. - -### Decoration - -- Resolve password once at construction: explicit option, then - `OPENCODE_SERVER_PASSWORD`, then no auth. -- Basic authentication uses user `opencode`. -- Per-call directory overrides client directory and is sent as `x-opencode-directory`. -- Every request carries `OpenCode.Sdk/` User-Agent. -- Decorate each request, never shared client headers. - -The exact safe extraction/sanitization of the informational version still needs an internal -implementation decision and tests. Do not silently substitute assembly version without noting -the behavioral difference. - -### Error channels - -API errors throw by default. `OpenCodeRequestOptions.NoThrow` is the only switch and applies per -call. It suppresses API-status exceptions only; usage, disposal, cancellation, transport, and -protocol failures still throw. - -`OpenCodeResponse` carries `Status`, `IsError`, typed `Error`, and nullable error-path `RawBody`. -Throwing API failures carry the same status/error/raw body on `OpenCodeApiException`. - -Only a tag allowed by that operation's status map becomes its concrete generated error type. An -unknown tag, undeclared status, or known tag returned at the wrong status becomes -`UnknownOpenCodeError` with exact tag and raw `JsonElement`. Malformed non-2xx JSON yields a null -typed error while preserving exact raw body. Unexpected or malformed 2xx is a protocol failure -wrapped as `OpenCodeTransportException`. Every `OperationCanceledException` passes through. - -## Selected Wire Contracts - -### `v2.health.get` - -| Fact | Value | -|---|---| -| Method/path | `GET /api/health` | -| Parameters/body | None | -| 200 | JSON `{ "healthy": true }`, classified `Bare` | -| 400 | `InvalidRequestError` | -| 401 | `UnauthorizedError` | -| Public owner/method | `OpenCodeClient.GetHealthAsync` | -| Response/payload | `HealthResponse.Healthy : bool` | - -The success root is promoted under: - -```text -op:v2.health.get#/responses/200/content/application~1json/schema -``` - -The pinned boolean is a one-value literal but intentionally emits `bool`, preserving a future -false value rather than rewriting it. - -### `v2.session.message` - -| Fact | Value | -|---|---| -| Method/path | `GET /api/session/{sessionID}/message/{messageID}` | -| Bound parameter | required path string `sessionID` | -| Method parameter | required path string `messageID` | -| Body/query | None | -| 200 | JSON `{ "data": SessionMessage }`, classified `Data` | -| 400 | `InvalidRequestError` | -| 401 | `UnauthorizedError` | -| 404 | `MessageNotFoundError` or `SessionNotFoundError` | -| Public owner/method | `SessionClient.GetMessageAsync(messageId, ...)` | -| Response/payload | `SessionMessageResponse.Message : SessionMessage` | - -The response roots are: - -```text -op:v2.session.message#/responses/200/content/application~1json/schema -op:v2.session.message#/responses/404/content/application~1json/schema -``` - -The raw 404 `anyOf` repeats `SessionNotFoundError`. Existing union normalization deduplicates it, -so the Binder must consume the semantic two-type set and must not re-read raw JSON to recover the -discarded duplicate. - -Relevant pinned source locations are `spec/openapi.json:9780` and -`spec/openapi.json:11449`. - -## Operation Binding Blueprint - -The following is a recommended minimal shape, not committed code. Keep it consumed-only and -adjust names only when an emitter demonstrably needs a different field. - -| Plan | Minimum consumed facts | -|---|---| -| `ClientPlan` | root property name, collection client type, optional handle type/factory, handle parameter | -| `OperationParameterPlan` | wire name, final C# name, `TypeReferencePlan`, handle-bound flag | -| `EnvelopePlan` | response type, success status/media, wire payload name, public payload name/type, docs | -| `ErrorVariantPlan` | wire tag and concrete generated type | -| `ErrorMapPlan` | status, media type, allowed error variants | -| `OperationPlan` | operation ID, owner type, method, HTTP method, route names/template, parameters, response/adapter, error maps, docs | - -Expected final values include: - -| Operation | Owner | Method | Response | Adapter | -|---|---|---|---|---| -| Health | `OpenCodeClient` | `GetHealthAsync` | `HealthResponse` | `HealthResponseAdapter` | -| Message | `SessionClient` | `GetMessageAsync` | `SessionMessageResponse` | `SessionMessageResponseAdapter` | - -The public route member names are not yet mechanically represented. Settle them before emission -because `OpenCodeRoutes` enters the API baseline. A coherent reference shape is nested -`OpenCodeRoutes.Health.Get` and `OpenCodeRoutes.Sessions.GetMessage(sessionId, messageId)`. -Nested public route classes trigger CA1034; use the repository's narrow, commented arbitration -pattern rather than weakening the rule globally. - -### Existing seam problem - -`SchemaPlanBinder` currently resolves final schema names internally and keeps `TypePlanBinder` as -a private nested class. `SchemaBindingResult` discards the name map. Operation binding needs the -same final types for path parameters and response payloads without duplicating schema policy. - -Recommended direction: - -1. Resolve schema names once per `SpecBinder.Bind` invocation. -2. Pass the same resolved map to both schema and operation binding. -3. Extract the nested type binder into a named internal collaborator or narrowly expose a - reusable type-resolution service. -4. Keep emitters entirely free of SpecIR and curation. - -Do not solve this by letting an operation emitter inspect schemas or by matching operation IDs. - -### Validation algorithm - -For each selected operation, bind or report located errors for all of these facts: - -1. Surface and placement: M1 supports modern ordinary operations; modern group curation chooses - root, collection, or declared handle. Legacy placement stays flat. -2. Transport shape: GET, no body, no query, no SSE/WebSocket/wildcard, one 200 success, JSON - response bodies. -3. Path integrity: every route token has exactly one required path parameter; M1 parameters - resolve to non-null string. -4. Handle binding: a declared handle parameter is removed from the method signature and stored - on the handle; remaining parameters stay on the generated method. -5. Success root: verify required properties and forbidden additional properties rather than - trusting envelope classification alone. -6. Error map: direct refs become one allowed variant; response-root marked unions flatten to a - deduplicated tag/type list; all concrete errors use the generated error hierarchy. -7. Naming: final owner, method, response, route, adapter, parameter, and payload names collide - nowhere in the selected output. -8. Ordering: clients/names ordinal, statuses numeric, variants ordinal by tag, generated paths - ordinal. - -Add an operation/protocol binding error category rather than mislabeling route/status/media -failures as schema errors. - -### Curation changes - -Add `[JsonPropertyName("handleParameter")] public string? HandleParameter` to `GroupCuration`. -Update curation validation so: - -- Root placement rejects client, handle, and handle-parameter fields. -- Client placement requires `clientName`. -- `handleName` and `handleParameter` are paired. -- The handle parameter names a required path parameter on at least one selected modern operation - in that group. -- Modern group coverage is selection-scoped during staged generation. -- Curation-to-spec orphan checks remain global. -- Legacy operations do not require or consume modern group-handle curation. - -Remove the current hard-coded `sessionID` detection from `CurationValidator`. - -## Runtime Core Blueprint - -Hand-written identity core: - -```text -ErrorBehavior.cs -OpenCodeClientOptions.cs -OpenCodeRequestOptions.cs -OpenCodeResponse.cs -OpenCodeException.cs -OpenCodeApiException.cs -OpenCodeTransportException.cs -Internal/Abstractions/IEnvironmentProvider.cs -Internal/SystemEnvironmentProvider.cs -Internal/Pipeline.cs -Internal/ResponseAdapter.cs -Properties/AssemblyInfo.cs -``` - -Generated callable surface: - -```text -OpenCodeClient.cs -SessionsClient.cs -SessionClient.cs -HealthResponse.cs -SessionMessageResponse.cs -OpenCodeRoutes.cs -Internal/ResponseAdapters/HealthResponseAdapter.cs -Internal/ResponseAdapters/SessionMessageResponseAdapter.cs -``` - -The agreed pipeline signature is: - -```csharp -Task ExecuteAsync( - HttpMethod method, - string route, - ResponseAdapter adapter, - OpenCodeRequestOptions? options, - CancellationToken cancellationToken) - where TResponse : OpenCodeResponse; -``` - -A small implementation is to buffer the M1 JSON body in `Pipeline`, then invoke a synchronous -adapter that maps `(status, rawBody)` to a typed envelope. Pipeline applies default throw versus -`NoThrow` after adaptation. This keeps HTTP ownership/channel policy in one place and -operation-specific JSON/status rules in generated adapters. - -Generated operation methods perform guards and one delegation only. `OperationMethodEmitter` -should return Roslyn method members consumed by `ClientEmitter`; do not emit partial operation -files, because file/type analyzers require one owner per client type. - -The exception constructors need deliberate treatment under CA1032 before the first API baseline. -Do not weaken the analyzer globally. The protected mock constructors must produce an instructive -`InvalidOperationException` when an unoverridden member reaches an absent pipeline. - -## Generated Ownership Changes - -`GenerationWriter.ValidateGeneratedSourcePath` currently admits only `Models/` and -`Internal/Serialization/`. Arc B needs root generated types and -`Internal/ResponseAdapters/` beside hand-written files. - -Before broadening the path policy: - -- Refuse writing an existing file that is not in the previous manifest. -- Require the exact two-line generator provenance header before overwriting or deleting a - manifest-owned source. -- Admit root generated `.cs` paths and the dedicated adapter subtree, not arbitrary - `Internal/` files. -- Preserve unsafe-path and symlink refusal. -- Treat duplicate generated paths case-insensitively at the `SourceEmitter` boundary, matching - writer ownership semantics. -- Test stale root/adapter deletion and preservation of hand-written root/internal files. - -`generate --verify` repairs owned drift and then reports it through before/after byte comparison; -it does not depend on Git. - -## Contract Test Matrix - -Use the real generated clients with a small recording `HttpMessageHandler`. Mock only the -environment seam. Keep wire JSON in embedded fixtures, never inline dumps. - -| Area | Required scenarios | -|---|---| -| Health | 200, 400, 401 under default and `NoThrow` | -| Message | 200, 400, 401, both 404 variants under default and `NoThrow` | -| Error skew | unknown tag, undeclared status, known tag at wrong status, malformed body | -| Envelopes | payload guard on error, `ToString` does not trip guarded getter, exact raw body | -| Routes | escape slash, space, percent, question mark in both IDs | -| Endpoint | trailing slash, no trailing slash, preserved path prefix, rejected query/fragment | -| Auth | explicit, environment fallback, explicit wins, one-time read, absent password | -| Directory | client default, per-call override, absent header | -| User-Agent | exact SDK product/version token on both operations | -| Cancellation | pre-cancelled and handler-blocked token; exception remains cancellation | -| Transport | `HttpRequestException`, malformed/unexpected success, response disposal | -| Ownership | owned client disposed; BYO client remains usable; wrappers reject use after root disposal | -| Mock seam | protected-constructor overrides and instructive unoverridden-member failure | -| API | one reviewed PublicApiGenerator/Verify baseline | - -Tests run on net8/net9/net10 locally; net472 runs on Windows CI and proxies netstandard2.0 runtime -behavior. `tests/OpenCode.Sdk.Tests/App.net472.config` is test-executable-only and must never flow -into the package. - -## Remaining Repository Work - -1. Clean and commit the current model-policy diff after full verification. -2. Implement curation-driven operation plans and Binder validation. -3. Add the net472 framework reference and hand-written HTTP core. -4. Add client/envelope/route/adapter emitters and generated ownership hardening. -5. Enable CS1591 for shipped source and document every hand-written public member. -6. Pin `PublicApiGenerator` 11.5.4 and add `Verify.TUnit` to SDK tests. -7. Add Linux CI `generate --verify` before build. -8. Regenerate, review the first public API baseline, and retain the partial packaging marker. -9. Run the complete repository gates. -10. Perform the real-server demonstration and move ROADMAP active work to M2. - -## Real-Server Demonstration - -`opencode` is installed at `/home/deniz/.nvm/versions/node/v24.18.0/bin/opencode`; the installed -version is 1.18.18 while the pinned spec snapshot is 1.18.15. Treat that version skew as visible -demo evidence; do not silently claim exact-pin execution. - -Start a password-enabled server in a directory containing at least one session message. Obtain -IDs with authenticated raw calls: - -```text -GET /api/session?limit=1 -GET /api/session/{sessionID}/message?limit=1 -``` - -Then use a temporary ignored console app to call only the generated health and single-message -methods and print `Healthy`, the concrete message type, and message `Id`. Paste exact commands and -output into the PR description. No automated process harness belongs in M1. - -## Final Gates - -Run these from the repository root: - -```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. Pack must fail only because -`src/OpenCode.Sdk/.generation-incomplete` records pending operations. - -## Stop Conditions - -Stop and involve the maintainer if any of these occurs: - -- Binder needs Microsoft.OpenApi or raw JSON to recover a fact SpecIR claims to provide. -- An emitter needs an operation ID, wire group, or concrete client-name branch. -- Supporting either selected operation needs a temporary public or transport API. -- The selected closure reaches a structural union. -- A selected response shape exceeds the declared M1 capability rather than failing closed. -- Generated ownership cannot distinguish hand-written files safely. -- Any product source fails on a locked target framework. -- Partial generation can pack successfully. -- Retry, telemetry, DI, launcher, SSE, or breadth becomes necessary merely to call the two M1 - operations. - -## Recommended First Actions For The Next Session - -1. Read `AGENTS.md`, the Arc B plan, ADR-0004/0007/0008, and this handoff. -2. Inspect `git status`, `git diff`, and commit `7f317f0`; do not assume the dirty files are all - intentional. -3. Separate the model-policy work from global formatter/analyzer collateral. -4. Run full build/tests/format/Slopwatch and commit the cleaned model-policy increment. -5. Red-test `handleParameter`, legacy-flat placement, the two pinned operation plans, and a - synthetic same-shape operation before implementing `OperationPlanBinder`. -6. Continue in the canonical plan's Task 1 order; delete this handoff only when the follow-up - work ships. From 40c6f0495b7d3ef43fdcbf0d4320f0fa523fe89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 15:39:12 +0300 Subject: [PATCH 07/28] feat(generator): admit handleParameter group curation Groups declaring a bound handle now pair handleName with handleParameter, and the validator checks the parameter names a required path parameter on at least one selected operation in the group (selection-scoped, client placement only). The hard-coded sessionID placement rule retires: a group without a handle declaration stays flat regardless of the path parameters its operations carry. --- .../2026-08-13-m1-arc-b-callable-client.md | 2 +- .../Generator/Binding/SpecBinderTests.cs | 111 +++++++++++++++++- .../Generator/Binding/CurationValidator.cs | 63 ++++++++-- .../Generator/Binding/Models/GroupCuration.cs | 3 + tools/curation.json | 3 +- 5 files changed, 168 insertions(+), 14 deletions(-) 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 index 03c8f7c1..aebbc8c9 100644 --- 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 @@ -77,7 +77,7 @@ and committed generated models. - [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. -- [ ] Add `handleParameter` to group curation. Require it and `handleName` together, +- [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. - [ ] Add consumed-only `ClientPlan`, `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs index 2cc37146..d7021349 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs @@ -241,6 +241,112 @@ await Assert.That(exception.Errors.Single(static error => error.Category == Bind .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_Allow_Flat_Client_Group_When_Selected_Operation_Carries_A_Path_Parameter() + { + 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, handleParameter: null), + }; + + var plan = new BindingTestHost().Bind(document, Selection("v2.session.message"), Curation(groups)); + + await Assert.That(plan.SelectedOperationIds.SequenceEqual(["v2.session.message"], StringComparer.Ordinal)).IsTrue(); + } + [Test] public async Task Bind_Should_Require_Curation_For_Every_Selected_Group() { @@ -327,11 +433,12 @@ private static GroupCuration RootGroup() => Placement = GroupPlacement.Root, }; - private static GroupCuration ClientGroup() => + private static GroupCuration ClientGroup(string? handleName = "SessionClient", string? handleParameter = "sessionID") => new() { Placement = GroupPlacement.Client, ClientName = "Sessions", - HandleName = "SessionClient", + HandleName = handleName, + HandleParameter = handleParameter, }; } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs index 085235f0..24652895 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs @@ -52,8 +52,8 @@ private static void ValidateGroupShape(string wireName, GroupCuration group, IRe { 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"); @@ -70,15 +70,58 @@ 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, "handleParameter cannot be blank"); + return; + } + + ValidateHandleParameterCoverage(wireName, group, selected, 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) + { + if (group.Placement is not GroupPlacement.Client) + { + 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, "session-scoped selected operation requires handleName"); + errors.Add( + BindingErrorCategory.Curation, + wireName, + $"handle parameter '{group.HandleParameter}' does not name a required path parameter on any selected operation in the group"); } } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/GroupCuration.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/GroupCuration.cs index 0270c47b..65802e80 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/curation.json b/tools/curation.json index 7bc92ebc..79e4cde2 100644 --- a/tools/curation.json +++ b/tools/curation.json @@ -6,7 +6,8 @@ "session": { "placement": "client", "clientName": "Sessions", - "handleName": "SessionClient" + "handleName": "SessionClient", + "handleParameter": "sessionID" } }, "envelopePayloadNames": { From b4cd3a792c54186b571c402b445bdb163f98a68e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 15:43:12 +0300 Subject: [PATCH 08/28] refactor(generator): resolve type names once per bind SpecBinder owns the SchemaNameResolver call and hands the resolved map to schema binding, so the upcoming operation binding consumes the same names instead of reinterpreting schemas. --- .../Support/BindingTestHost.cs | 4 ++-- .../Generator/Binding/SchemaPlanBinder.cs | 19 ++++++++++--------- .../Generator/Binding/SpecBinder.cs | 8 +++++++- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs index 7e7090bc..e671fe49 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs @@ -12,11 +12,11 @@ internal sealed class BindingTestHost public BindingTestHost() { - var schemaNames = new SchemaNameResolver(); _binder = new SpecBinder( new ReachableSchemaCollector(), new CurationValidator(), - new SchemaPlanBinder(schemaNames)); + new SchemaNameResolver(), + new SchemaPlanBinder()); } public EmitPlan Bind(SpecDocument document, OperationSelection selection, GenerationCuration curation) diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs index 1821f1e0..e6d0e3e1 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs @@ -3,20 +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) + public SchemaBindingResult Bind(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation, + 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); @@ -52,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 { diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs index d2b4662f..42e5bc6e 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs @@ -8,10 +8,12 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; internal sealed class SpecBinder( ReachableSchemaCollector reachableSchemas, CurationValidator curationValidator, + SchemaNameResolver schemaNames, SchemaPlanBinder schemaPlans) : ISpecBinder { private readonly CurationValidator _curationValidator = curationValidator ?? throw new ArgumentNullException(nameof(curationValidator)); 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) @@ -25,7 +27,11 @@ 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 selectedIds = selected.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal); var pending = document.Operations .Where(operation => !selectedIds.Contains(operation.OperationId)) From ec2bb29681c5bef1c3a2046f20fac3446f013532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 16:02:37 +0300 Subject: [PATCH 09/28] feat(generator): bind the selected operations into client plans Consumed-only ClientPlan/OperationPlan/OperationParameterPlan/EnvelopePlan/ ErrorMapPlan land behind an OperationPlanBinder that consumes the shared type-name map and fails closed on anything but ordinary GET + JSON + path-parameter operations with one 200 success. Wire-contract refusals get their own Operation error category and batch per operation. Envelope payload names now derive mechanically from the operation subject (Health, Message); envelopePayloadNames rows are overrides for response-spine collisions only, and the required-per-operation validation retires (maintainer, 2026-08-13). Groups place operations through the general handle rule; a synthetic same-shape group binds through the same code with zero operation-, group-, or client-specific branches. --- .../2026-08-13-m1-arc-b-callable-client.md | 23 +- .../Binding/OperationPlanBinderTests.cs | 471 +++++++++++++++ .../Generator/Binding/SpecBinderTests.cs | 79 +-- .../Support/BindingScenarioData.cs | 45 ++ .../Support/BindingTestHost.cs | 11 +- .../Support/EmitterPlanFixture.cs | 1 + .../Support/GenerationTestData.cs | 5 +- .../Support/OperationBuilder.cs | 6 + .../Support/OperationResponseBuilder.cs | 7 + .../Generator/Binding/CurationValidator.cs | 22 +- .../Binding/Models/BindingErrorCategory.cs | 3 + .../Generator/Binding/Models/ClientPlan.cs | 37 ++ .../Binding/Models/ClientReferencePlan.cs | 8 + .../Generator/Binding/Models/ClientRole.cs | 8 + .../Generator/Binding/Models/EmitPlan.cs | 10 + .../Generator/Binding/Models/EnvelopePlan.cs | 15 + .../Generator/Binding/Models/ErrorMapPlan.cs | 14 + .../Binding/Models/ErrorStatusPlan.cs | 16 + .../Generator/Binding/Models/ErrorTagPlan.cs | 8 + .../Binding/Models/HandleFactoryPlan.cs | 10 + .../Binding/Models/OperationParameterPlan.cs | 13 + .../Generator/Binding/Models/OperationPlan.cs | 30 + .../Generator/Binding/OperationNamePolicy.cs | 43 ++ .../Generator/Binding/OperationPlanBinder.cs | 542 ++++++++++++++++++ .../Generator/Binding/SpecBinder.cs | 6 +- tools/OpenCode.Sdk.Tools/ToolApp.cs | 1 + tools/curation.json | 4 +- 27 files changed, 1339 insertions(+), 99 deletions(-) create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Support/BindingScenarioData.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientPlan.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientReferencePlan.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ClientRole.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/EnvelopePlan.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorMapPlan.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorStatusPlan.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/ErrorTagPlan.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/HandleFactoryPlan.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationParameterPlan.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationPlan.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs 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 index aebbc8c9..22621c93 100644 --- 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 @@ -18,6 +18,12 @@ names, or concrete client names. Generated methods delegate once into a hand-wri `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. @@ -49,10 +55,13 @@ evidence: research doc 15). - [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. -- [ ] Re-derive the selected wire contracts from the new pin: `v2.health.get` success is the +- [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 are re-read from the v2 responses — do not assume the 1.x tables. + 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 @@ -80,13 +89,13 @@ and committed generated models. - [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. -- [ ] Add consumed-only `ClientPlan`, `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, +- [x] Add consumed-only `ClientPlan`, `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, and `ErrorMapPlan` plus an `OperationPlanBinder`. Reuse schema-resolved type names instead of reinterpreting schemas. -- [ ] 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`, curated - `{data}` to `SessionMessageResponse.Message`, and the error maps Task 0 derived. -- [ ] Test complete plans, handle-versus-method parameters, collisions, documentation, status +- [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`. 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 00000000..ae498905 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs @@ -0,0 +1,471 @@ +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.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.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.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_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 d7021349..f40aee09 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; @@ -74,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] @@ -117,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() { @@ -329,24 +314,6 @@ await Assert.That(exception.Errors.Single(static error => error.Category == Bind .Contains("required path parameter"); } - [Test] - public async Task Bind_Should_Allow_Flat_Client_Group_When_Selected_Operation_Carries_A_Path_Parameter() - { - 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, handleParameter: null), - }; - - var plan = new BindingTestHost().Bind(document, Selection("v2.session.message"), Curation(groups)); - - await Assert.That(plan.SelectedOperationIds.SequenceEqual(["v2.session.message"], StringComparer.Ordinal)).IsTrue(); - } - [Test] public async Task Bind_Should_Require_Curation_For_Every_Selected_Group() { @@ -401,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() { @@ -426,19 +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(string? handleName = "SessionClient", string? handleParameter = "sessionID") => - new() - { - Placement = GroupPlacement.Client, - ClientName = "Sessions", - HandleName = handleName, - HandleParameter = handleParameter, - }; } 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 00000000..d800cbc2 --- /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 e671fe49..c5ee5ed4 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs @@ -16,7 +16,8 @@ public BindingTestHost() new ReachableSchemaCollector(), new CurationValidator(), new SchemaNameResolver(), - new SchemaPlanBinder()); + 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 df2f3725..58ffeb1f 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs @@ -41,6 +41,7 @@ public static EmitPlan Create() "UnknownOpenCodeError", ], }, + Clients = [], PendingOperations = [], }; } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs index 70440afb..7678261d 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs @@ -34,7 +34,10 @@ public static MockFileSystem CreateFileSystem() public static MockFileSystem CreateCommandFileSystem() { var spec = new SpecDocumentBuilder() - .WithOperation("v2.health.get", path: "/api/health") + .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(); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/OperationBuilder.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/OperationBuilder.cs index 9b7d5e91..da95191c 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/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs index 24652895..02cde23e 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); } @@ -125,20 +125,14 @@ private static void ValidateHandleParameterCoverage(string wireName, GroupCurati } } - private static void ValidateEnvelopeNames(IReadOnlyList selected, HashSet selectedIds, - HashSet documentIds, GenerationCuration curation, BindingErrorCollector errors) + /// + /// 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 operation in selected) - { - 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"); - } - } - foreach (var (operationId, payloadName) in curation.EnvelopePayloadNames.OrderBy(static pair => pair.Key, StringComparer.Ordinal)) { if (!documentIds.Contains(operationId)) diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/BindingErrorCategory.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/BindingErrorCategory.cs index d231e729..18f7533c 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 00000000..b69f3236 --- /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 00000000..05711244 --- /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 00000000..6acffa45 --- /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 aec3e283..d639a351 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 00000000..e17e5025 --- /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 00000000..2ef916e4 --- /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 00000000..2ae45597 --- /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 00000000..a78d1d97 --- /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/HandleFactoryPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/HandleFactoryPlan.cs new file mode 100644 index 00000000..fab08496 --- /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 00000000..bd9527f7 --- /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 00000000..53edf203 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationPlan.cs @@ -0,0 +1,30 @@ +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 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/OperationNamePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs new file mode 100644 index 00000000..222af23e --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs @@ -0,0 +1,43 @@ +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 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 00000000..cbad77ab --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs @@ -0,0 +1,542 @@ +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}'"); + } + } + + 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, + 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/SpecBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs index 42e5bc6e..6cbacb5a 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs @@ -9,9 +9,11 @@ internal sealed class SpecBinder( ReachableSchemaCollector reachableSchemas, CurationValidator curationValidator, SchemaNameResolver schemaNames, - SchemaPlanBinder schemaPlans) : ISpecBinder + 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)); @@ -32,6 +34,7 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera // 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)) @@ -49,6 +52,7 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera Models = schemaResult.Models, Unions = schemaResult.Unions, Registry = schemaResult.Registry, + Clients = clients, PendingOperations = pending, }; } diff --git a/tools/OpenCode.Sdk.Tools/ToolApp.cs b/tools/OpenCode.Sdk.Tools/ToolApp.cs index e2564c6b..d7a336b5 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 79e4cde2..29192073 100644 --- a/tools/curation.json +++ b/tools/curation.json @@ -10,9 +10,7 @@ "handleParameter": "sessionID" } }, - "envelopePayloadNames": { - "v2.session.message": "Message" - }, + "envelopePayloadNames": {}, "propertyOverrides": [ { "schema": "Prompt.FileSource#/anyOf/type=uri", From a2dfb91bf12d1d5210b2086c792ac8aa171cd7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 16:17:22 +0300 Subject: [PATCH 10/28] feat(sdk): land the response, options, and exception spine Hand-written identity core, first slice: ErrorBehavior, client/request options with the shared NoThrow instance, the OpenCodeResponse envelope base, and the OpenCodeException spine carrying status, typed error, and raw body. The environment seam (IEnvironmentProvider + SystemEnvironmentProvider) lands for the one-time password fallback, net472 gains its conditional System.Net.Http framework reference, and the SDK now hides source-only polyfills behind the embedded attribute because it declares a friend test assembly. --- .../14-polyfill-capability-inventory.md | 2 +- src/OpenCode.Sdk/ErrorBehavior.cs | 11 +++++ .../Abstractions/IEnvironmentProvider.cs | 10 ++++ .../Internal/SystemEnvironmentProvider.cs | 14 ++++++ src/OpenCode.Sdk/OpenCode.Sdk.csproj | 7 +++ src/OpenCode.Sdk/OpenCodeApiException.cs | 49 +++++++++++++++++++ src/OpenCode.Sdk/OpenCodeClientOptions.cs | 19 +++++++ src/OpenCode.Sdk/OpenCodeException.cs | 25 ++++++++++ src/OpenCode.Sdk/OpenCodeRequestOptions.cs | 14 ++++++ src/OpenCode.Sdk/OpenCodeResponse.cs | 19 +++++++ .../OpenCodeTransportException.cs | 25 ++++++++++ src/OpenCode.Sdk/Properties/AssemblyInfo.cs | 3 ++ .../OpenCodeApiExceptionTests.cs | 49 +++++++++++++++++++ .../OpenCodeRequestOptionsTests.cs | 24 +++++++++ .../OpenCodeResponseTests.cs | 20 ++++++++ .../SystemEnvironmentProviderTests.cs | 37 ++++++++++++++ 16 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 src/OpenCode.Sdk/ErrorBehavior.cs create mode 100644 src/OpenCode.Sdk/Internal/Abstractions/IEnvironmentProvider.cs create mode 100644 src/OpenCode.Sdk/Internal/SystemEnvironmentProvider.cs create mode 100644 src/OpenCode.Sdk/OpenCodeApiException.cs create mode 100644 src/OpenCode.Sdk/OpenCodeClientOptions.cs create mode 100644 src/OpenCode.Sdk/OpenCodeException.cs create mode 100644 src/OpenCode.Sdk/OpenCodeRequestOptions.cs create mode 100644 src/OpenCode.Sdk/OpenCodeResponse.cs create mode 100644 src/OpenCode.Sdk/OpenCodeTransportException.cs create mode 100644 src/OpenCode.Sdk/Properties/AssemblyInfo.cs create mode 100644 tests/OpenCode.Sdk.Tests/OpenCodeApiExceptionTests.cs create mode 100644 tests/OpenCode.Sdk.Tests/OpenCodeRequestOptionsTests.cs create mode 100644 tests/OpenCode.Sdk.Tests/OpenCodeResponseTests.cs create mode 100644 tests/OpenCode.Sdk.Tests/SystemEnvironmentProviderTests.cs diff --git a/docs/research/14-polyfill-capability-inventory.md b/docs/research/14-polyfill-capability-inventory.md index 4cd8a4dc..b9c301c3 100644 --- a/docs/research/14-polyfill-capability-inventory.md +++ b/docs/research/14-polyfill-capability-inventory.md @@ -56,7 +56,7 @@ other optional groups opt-in by removing their source when the corresponding pro | `PolyNullability` | Unset | Disabled | | `PolyStringInterpolation` | Unset | Disabled | | `PolyPublic` | Unset | Polyfill types remain internal | -| `PolyUseEmbeddedAttribute` | Unset in product; `true` in the tooling project | Product uses ordinary internal types; tooling hides them from friend assemblies | +| `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 diff --git a/src/OpenCode.Sdk/ErrorBehavior.cs b/src/OpenCode.Sdk/ErrorBehavior.cs new file mode 100644 index 00000000..fc03561a --- /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/Internal/Abstractions/IEnvironmentProvider.cs b/src/OpenCode.Sdk/Internal/Abstractions/IEnvironmentProvider.cs new file mode 100644 index 00000000..8996eccd --- /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/SystemEnvironmentProvider.cs b/src/OpenCode.Sdk/Internal/SystemEnvironmentProvider.cs new file mode 100644 index 00000000..c74b5b1c --- /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/OpenCode.Sdk.csproj b/src/OpenCode.Sdk/OpenCode.Sdk.csproj index bfc3d1be..93b7ac54 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,9 @@ + + + + + diff --git a/src/OpenCode.Sdk/OpenCodeApiException.cs b/src/OpenCode.Sdk/OpenCodeApiException.cs new file mode 100644 index 00000000..6511eb3c --- /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/OpenCodeClientOptions.cs b/src/OpenCode.Sdk/OpenCodeClientOptions.cs new file mode 100644 index 00000000..b6524f2d --- /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 00000000..bcf375f8 --- /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 00000000..41e88a0b --- /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 00000000..8cd085ec --- /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/OpenCodeTransportException.cs b/src/OpenCode.Sdk/OpenCodeTransportException.cs new file mode 100644 index 00000000..8f916853 --- /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 00000000..93c55e33 --- /dev/null +++ b/src/OpenCode.Sdk/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("OpenCode.Sdk.Tests")] diff --git a/tests/OpenCode.Sdk.Tests/OpenCodeApiExceptionTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeApiExceptionTests.cs new file mode 100644 index 00000000..267882b7 --- /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/OpenCodeRequestOptionsTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeRequestOptionsTests.cs new file mode 100644 index 00000000..c1b3f461 --- /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 00000000..f6625148 --- /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/SystemEnvironmentProviderTests.cs b/tests/OpenCode.Sdk.Tests/SystemEnvironmentProviderTests.cs new file mode 100644 index 00000000..60e30a9c --- /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(" ")); + } +} From 3e0c21c29e6e8e5f4506765a5a9888572d142862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 16:28:11 +0300 Subject: [PATCH 11/28] feat(sdk): implement the Pipeline behavior core Pipeline.ExecuteAsync is the single home of request behavior: endpoint validation and route joining through EndpointPolicy (absolute HTTP(S), no query/fragment/userinfo, path prefix preserved), per-request Basic-auth and User-Agent decoration (explicit password before the one-time OPENCODE_SERVER_PASSWORD read; UserAgentPolicy strips build metadata and omits the version token when unresolvable - maintainer, 2026-08-13), one send primitive with ResponseHeadersRead, buffered bodies handed to a synchronous ResponseAdapter, and throw-versus-NoThrow applied after adaptation. Transport failures wrap as OpenCodeTransportException; cancellation always passes through. Owned clients dispose with the pipeline, injected clients never; a disposed pipeline refuses execution. Downlevel targets gain the System.Net.Http global using implicit usings only provide on net6+. --- .editorconfig | 7 + .../2026-08-13-m1-arc-b-callable-client.md | 10 +- src/OpenCode.Sdk/Internal/EndpointPolicy.cs | 40 +++ src/OpenCode.Sdk/Internal/Pipeline.cs | 163 +++++++++ src/OpenCode.Sdk/Internal/ResponseAdapter.cs | 13 + src/OpenCode.Sdk/Internal/UserAgentPolicy.cs | 31 ++ src/OpenCode.Sdk/OpenCode.Sdk.csproj | 5 + src/OpenCode.Sdk/Properties/AssemblyInfo.cs | 3 + .../OpenCode.Sdk.Tests/EndpointPolicyTests.cs | 58 ++++ tests/OpenCode.Sdk.Tests/PipelineTests.cs | 325 ++++++++++++++++++ .../Support/RecordedRequest.cs | 12 + .../Support/RecordingHttpHandler.cs | 46 +++ .../Support/RecordingResponseAdapter.cs | 34 ++ .../Support/TestResponse.cs | 3 + .../UserAgentPolicyTests.cs | 39 +++ 15 files changed, 784 insertions(+), 5 deletions(-) create mode 100644 src/OpenCode.Sdk/Internal/EndpointPolicy.cs create mode 100644 src/OpenCode.Sdk/Internal/Pipeline.cs create mode 100644 src/OpenCode.Sdk/Internal/ResponseAdapter.cs create mode 100644 src/OpenCode.Sdk/Internal/UserAgentPolicy.cs create mode 100644 tests/OpenCode.Sdk.Tests/EndpointPolicyTests.cs create mode 100644 tests/OpenCode.Sdk.Tests/PipelineTests.cs create mode 100644 tests/OpenCode.Sdk.Tests/Support/RecordedRequest.cs create mode 100644 tests/OpenCode.Sdk.Tests/Support/RecordingHttpHandler.cs create mode 100644 tests/OpenCode.Sdk.Tests/Support/RecordingResponseAdapter.cs create mode 100644 tests/OpenCode.Sdk.Tests/Support/TestResponse.cs create mode 100644 tests/OpenCode.Sdk.Tests/UserAgentPolicyTests.cs diff --git a/.editorconfig b/.editorconfig index 734a0360..73f87b3b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -781,3 +781,10 @@ 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 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 index 22621c93..3d4135f4 100644 --- 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 @@ -106,19 +106,19 @@ and committed generated models. `Internal/Abstractions/IEnvironmentProvider`, `Internal/{Pipeline,ResponseAdapter,SystemEnvironmentProvider}`, friend metadata, and tests. -- [ ] Add the conditional `net472` framework reference. Add `ErrorBehavior`, client/request +- [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. -- [ ] Implement endpoint validation/joining and owned-versus-injected `HttpClient` lifetime. +- [x] Implement endpoint validation/joining and owned-versus-injected `HttpClient` lifetime. Disposing the root makes its wrappers unusable and never disposes a BYO client. -- [ ] Resolve explicit password before one-time `OPENCODE_SERVER_PASSWORD` fallback. Decorate +- [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. -- [ ] Use only `HttpClient.SendAsync(request, ResponseHeadersRead, cancellationToken)`. Own each +- [x] Use only `HttpClient.SendAsync(request, ResponseHeadersRead, cancellationToken)`. Own each request and buffered response, preserve cancellation, and wrap network/protocol failures as `OpenCodeTransportException`. -- [ ] Centralize default throw versus per-call `NoThrow` in +- [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. diff --git a/src/OpenCode.Sdk/Internal/EndpointPolicy.cs b/src/OpenCode.Sdk/Internal/EndpointPolicy.cs new file mode 100644 index 00000000..fedae303 --- /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/Pipeline.cs b/src/OpenCode.Sdk/Internal/Pipeline.cs new file mode 100644 index 00000000..82c522f5 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Pipeline.cs @@ -0,0 +1,163 @@ +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)); + } + + 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 && (options?.ErrorBehavior ?? ErrorBehavior.Default) 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 (HttpRequestException exception) + { + // OperationCanceledException deliberately 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) + { + // 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 00000000..de42a07b --- /dev/null +++ b/src/OpenCode.Sdk/Internal/ResponseAdapter.cs @@ -0,0 +1,13 @@ +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); +} diff --git a/src/OpenCode.Sdk/Internal/UserAgentPolicy.cs b/src/OpenCode.Sdk/Internal/UserAgentPolicy.cs new file mode 100644 index 00000000..178906f8 --- /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/OpenCode.Sdk.csproj b/src/OpenCode.Sdk/OpenCode.Sdk.csproj index 93b7ac54..d2418518 100644 --- a/src/OpenCode.Sdk/OpenCode.Sdk.csproj +++ b/src/OpenCode.Sdk/OpenCode.Sdk.csproj @@ -20,4 +20,9 @@ + + + + + diff --git a/src/OpenCode.Sdk/Properties/AssemblyInfo.cs b/src/OpenCode.Sdk/Properties/AssemblyInfo.cs index 93c55e33..fb330a45 100644 --- a/src/OpenCode.Sdk/Properties/AssemblyInfo.cs +++ b/src/OpenCode.Sdk/Properties/AssemblyInfo.cs @@ -1,3 +1,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("OpenCode.Sdk.Tests")] + +// NSubstitute proxies internal seam interfaces through Castle DynamicProxy. +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/tests/OpenCode.Sdk.Tests/EndpointPolicyTests.cs b/tests/OpenCode.Sdk.Tests/EndpointPolicyTests.cs new file mode 100644 index 00000000..6eca3ddf --- /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/PipelineTests.cs b/tests/OpenCode.Sdk.Tests/PipelineTests.cs new file mode 100644 index 00000000..5f021a4b --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/PipelineTests.cs @@ -0,0 +1,325 @@ +using System.Net; +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_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/Support/RecordedRequest.cs b/tests/OpenCode.Sdk.Tests/Support/RecordedRequest.cs new file mode 100644 index 00000000..4aef9b07 --- /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 00000000..1eafbe11 --- /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 00000000..15a36e57 --- /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 00000000..b94976b1 --- /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/UserAgentPolicyTests.cs b/tests/OpenCode.Sdk.Tests/UserAgentPolicyTests.cs new file mode 100644 index 00000000..065cac26 --- /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(); + } +} From aa7e3515bbe35dc45509ef8f96e8dd31fc7a5cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 16:34:46 +0300 Subject: [PATCH 12/28] feat(generator): plan route container and member names OperationPlan carries RouteContainerName (the client name, or the group for root placement) and RouteMemberName (verb plus subject, never restating the container). Containers refuse duplicate members alongside the existing method, response, and client collision checks. --- .../Binding/OperationPlanBinderTests.cs | 23 +++++++++++++++++++ .../Generator/Binding/Models/OperationPlan.cs | 6 +++++ .../Generator/Binding/OperationNamePolicy.cs | 7 ++++++ .../Generator/Binding/OperationPlanBinder.cs | 12 ++++++++++ 4 files changed, 48 insertions(+) diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs index ae498905..533fd91f 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs @@ -27,6 +27,8 @@ await Assert.That(plan.Clients.Select(static client => client.Name) 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(); @@ -58,6 +60,8 @@ await Assert.That(health.ErrorMap.Statuses.Select(static status => status.Status 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(); @@ -100,6 +104,8 @@ public async Task Bind_Should_Bind_A_Synthetic_Same_Shape_Group_Through_The_Same 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"); @@ -327,6 +333,23 @@ await Assert.That(exception.Errors.Any(static error => error.Category == Binding && 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() { diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationPlan.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationPlan.cs index 53edf203..9c0acc38 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationPlan.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/OperationPlan.cs @@ -9,6 +9,12 @@ internal sealed record OperationPlan 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 { diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs index 222af23e..8614d342 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationNamePolicy.cs @@ -26,6 +26,13 @@ public static string ResponseTypeName(SpecOperation 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); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs index cbad77ab..3a946b98 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs @@ -171,6 +171,16 @@ private void CheckTypeNameCollisions(List clients, List(_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 @@ -230,6 +240,8 @@ private sealed class SingleOperationBinder(SpecDocument document, SpecOperation 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, From 48b572a8667a2e497ef205146c66c229118751cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 16:39:11 +0300 Subject: [PATCH 13/28] docs(agents): refresh the Arc B handover to the post-Task-2 state --- .../handover-prompts/HANDOFF-2026-08-13-2.md | 289 ++++++------------ 1 file changed, 97 insertions(+), 192 deletions(-) diff --git a/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md b/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md index 41f8c511..67454952 100644 --- a/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md +++ b/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md @@ -1,200 +1,118 @@ -# M1 Arc B Handoff — Post-Retarget (Tasks 1–4) +# M1 Arc B Handoff — Tasks 3b–4 Date: 2026-08-13 Consume this document against live Git and delete it when Arc B ships. It records unfinished -session state, not evergreen architecture. It supersedes `HANDOFF-2026-08-13.md` (deleted in -the same commit; its Task 1 model-policy work and the wire-contract re-derivation it planned -have landed). +session state, not evergreen architecture. ## Onboarding Reading Order -1. `AGENTS.md` — locked decisions. Note: ADR-0005 was **revised in place** on 2026-08-13; the - SDK now targets the opencode v2 protocol surface only. There is no legacy surface, no - dual-surface machinery, and no v1 convention debt anywhere. -2. `docs/adr/0005-v2-protocol-surface-only.md` (the revision), plus ADRs 0003/0004/0007/0008/ - 0009 — the binding design spine. +1. `AGENTS.md` — locked decisions (ADR-0005 targets the v2 protocol surface only). +2. ADRs 0003/0004/0005/0007/0008/0009 — the binding design spine. 3. `docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md` — the canonical execution - plan. Task 0 and the two model-policy items of Task 1 are checked off; everything unchecked - is this handoff's work. -4. `docs/research/15-opencode-v2-platform.md` — the canonical picture of the v2 platform - (branch, architecture, distribution, dialect, live verification transcript). -5. Research log Session 17 (`docs/research/00-research-log.md`) — the retarget decision chain. -6. `docs/engineering/coding-style.md` and `docs/engineering/testing-style.md` — before writing + plan. Tasks 0–2 are fully checked; Task 3 is in flight (route naming landed); everything + unchecked is this handoff's work. +4. `docs/research/15-opencode-v2-platform.md` — the canonical v2 platform picture. +5. `docs/engineering/coding-style.md` and `docs/engineering/testing-style.md` — before writing any code. -Historical context (read when needed, never as current fact): research docs 09/10 are the -dated v1-line genealogy; the three `docs/superpowers/specs/` documents are vision/reference -with a banner marking their v1 dual-surface facts historical; sessions 12/13 in the research -log record the ingestion redesign the current `Generator/Ingestion` implements. - ## Git And Workspace State | Item | State | |---|---| | Workspace | `/home/deniz/src/opencode-sdk-dotnet` | | Branch | `feature/m1-callable-client` | -| HEAD | `911b7f3 feat(generator): retarget the pin to the opencode v2 protocol surface` | -| Ahead of `origin/master` (`3e3e058`) | 5 commits, no remote branch, no pushes | -| Worktree | Clean | -| Gates at HEAD | generate ✓, `generate --verify` current ✓, Release build 0W/0E ✓, tools tests 221 ✓, SDK tests 45 ✓ (net8/9/10), format ✓, Slopwatch 0 ✓ | +| HEAD | `aa7e351 feat(generator): plan route container and member names` | +| Ahead of `origin/master` | 13 commits, no remote branch, no pushes | +| Gates at HEAD | all six green (441 tests; net8/9/10 SDK legs, tools) | The maintainer authorized implementation commits inside this development loop; pushing still -requires an explicit request. SSH operations here may need `GIT_SSH_COMMAND='ssh -F /dev/null'`. - -The five commits: `f35f741` (Polyfill research), `7f317f0` (Arc B plan), `d0865e9` (model -policy: PascalCase acronyms + `OptionalCollectionInput`, plus the retarget decision's docs -pass), `5f71294` (sealed conventions: no taste curation, v1 ledger closed, surface-split -retirement), `911b7f3` (Task 0: the retarget itself). - -## The Pin And What Task 0 Built - -`spec/openapi.json` is `packages/protocol/openapi.json` at `anomalyco/opencode` v2-branch -commit `a6a712a3ac72248c9b2f2f883e752e6e18ef8c40` (provenance: `spec/SNAPSHOT.md`). 104 paths / -120 operations / 324 schemas, all `/api`-mounted, all operationIds `v2.`-prefixed. `generate` -reports: Selected 2, Pending 118. Refreshes are deliberate snapshot moves (M2 boundary), never -HEAD-tracking. - -Task 0 changes an incoming agent must know before touching the generator: - -- **Ingestion-wall admits** (each fail-closed to the observed population, all red-tested): - a single-element validation-only `allOf` wrapper unwraps into its host - (`SchemaShapeClassifier.IsValidationOnlyAllOfWrapper`); a single-value number/integer enum - projects as `LiteralKind.Number`; a bare `{"type":"array"}` projects as array-of-any-value; - `prefixItems` admits exactly two shapes in `PrefixItemsAdapter` — the closed tuple - (`minItems == maxItems == arity`, no `items`) and Effect's NonEmptyArray encoding (single - prefix element sharing the `items` `$ref` target → plain `ArrayNode`); the old - location-pinned admission is retired, shape validation replaced it. -- **Union machinery for the v2 topology** (`SchemaPlanBinder`, `UnionNormalizer`, - `UnionEmitter`, `ModelEmitter`): the discriminating marker is *selected* (first property, in - the first branch's document order, that every branch carries with one kind and pairwise - distinct values — `status` beats the duplicated `type` on the Compaction family); a union - branch may be a **nested marked union** (exactly one level): `SessionMessageCompaction : - SessionMessageInfo` carries `BaseTypeName` + `FixedMarker` (`type=compaction`) on its - `UnionPlan`, its variants override both chain markers, its unknown carrier fixes the outer - tag; promoted inline branches are named root + marker value (`PromptFileSourceInline`/`Uri`); - the special-number rule accepts v2's one-level-nested encoding. -- **Two serialization bugs were caught by the new nested-union tests and fixed in the - emitter** — unknown-carrier marker overrides now emit `[JsonPropertyName]` (STJ otherwise - fails constructor binding), and a union converter's `Write` serializes a nested-union - variant through its **declared base type's** typed context property so the nested - converter's raw-payload path runs (runtime-type metadata would serialize the carrier as an - ordinary record). Both behaviors are covered by `OpenCode.Sdk.Tests` serialization tests. -- **Single surface**: `SpecSurface` is deleted; `OperationIdentityParser` refuses any - operationId without the `v2.` prefix (located error); `GenerationReport` carries one - `PendingOperationIds`; the `.generation-incomplete` marker and the CLI print one "Pending - operations" count. The word "legacy" does not exist in production code. -- **Writer semantics** (`GenerationWriter`): manifest-diff cleanup — files in the previous - manifest but absent from the new plan are deleted; the writer touches nothing outside the - manifest and its admitted subtrees (`Models/`, `Internal/Serialization/`). Task 3 broadens - ownership and must add the unmanifested-overwrite refusal and provenance-header checks. - -## Selected Wire Contracts (v2) - -| Fact | `v2.health.get` | `v2.session.message` | -|---|---|---| -| Route | GET `/api/health` | GET `/api/session/{sessionID}/message/{messageID}` | -| 200 | `$ref ServiceHealth` — `{healthy, version, pid}` (observed live) | `{data: Session.Message.Info}`, classified Data | -| Owner/method | `OpenCodeClient.GetHealthAsync` | `SessionClient.GetMessageAsync(messageId, …)` | -| Response | `HealthResponse` | `SessionMessageResponse.Message` | - -**The error maps are the one unchecked Task 0 box:** they have NOT been derived from the v2 -spec. Do not assume the 1.x tables (400/401/404 and the duplicated-404 union were v1 facts). -Read the v2 responses of both operations from the pin as the first step of operation binding. - -## Remaining Work (Arc B Tasks 1–4) - -The plan document is canonical; this is the operational detail on top of it. - -### Task 1 remainder — operation binding - -- `handleParameter` joins `GroupCuration` (`[JsonPropertyName("handleParameter")]`). - Validation: root placement rejects client/handle fields; client placement requires - `clientName`; `handleName` and `handleParameter` are paired; the handle parameter names a - required path parameter on at least one selected operation in that group; curation-to-spec - orphan checks stay global; selection-scoped coverage during staged generation. -- Consumed-only plan types (`ClientPlan`, `OperationPlan`, `OperationParameterPlan`, - `EnvelopePlan`, `ErrorMapPlan`) + `OperationPlanBinder`. Known seam: `SchemaPlanBinder` - still resolves type names internally (`TypePlanBinder` is a private nested class; - `SchemaBindingResult` discards the name map). Resolve names once per `SpecBinder.Bind`, pass - the same map to schema and operation binding; do not let operation binding reinterpret - schemas or read raw OpenAPI. -- Bind only ordinary GET + JSON + path-parameter operations with one 200 success, no - body/query/stream/wildcard. Handle-declared groups put the handle parameter on the handle; - everything else stays flat. Add an operation/protocol binding error category — do not - mislabel route/status/media failures as schema errors. -- Tests: complete pinned plans, handle-versus-method parameters, collisions, documentation, - status maps, batched failures, determinism; a synthetic same-shape operation with different - names binds without new production branches; an operation in a group without a handle - declaration stays flat even when it carries `sessionID`. -- Blueprint (recommended, consumed-only): expected final values `OpenCodeClient.GetHealthAsync` - → `HealthResponse` / `HealthResponseAdapter`; `SessionClient.GetMessageAsync` → - `SessionMessageResponse` / `SessionMessageResponseAdapter`. Route members enter the API - baseline — settle their shape before emission (reference shape: nested - `OpenCodeRoutes.Health.Get`, `OpenCodeRoutes.Sessions.GetMessage(sessionId, messageId)`; - nested public classes trip CA1034 — use the narrow arbitration pattern, never a global - weakening). - -### Task 2 — hand-written HTTP core - -Identity files per the plan (options/response/exception spine, `IEnvironmentProvider`, -`Pipeline`, `ResponseAdapter`, `SystemEnvironmentProvider`, friend metadata). Locked -decisions that survive the retarget verbatim: URI constructors own endpoint authority; BYO -`HttpClient` requires `options.Endpoint`; never touch `BaseAddress`/`DefaultRequestHeaders`; -absolute HTTP(S) endpoints without query/fragment, normalized path prefix preserved; owned -client disposed, injected never; root disposal marks wrappers unusable; single send primitive -`SendAsync(request, ResponseHeadersRead, ct)`; Basic auth user `opencode`, explicit password -before one-time `OPENCODE_SERVER_PASSWORD` fallback; per-request decoration, never shared -headers; `OpenCode.Sdk/` User-Agent (the safe informational-version -extraction still needs an explicit decision + tests — do not silently substitute assembly -version). **Changed by the retarget:** there is no `x-opencode-directory` header — per-request -scoping rides the v2 `location[...]` query convention, bound as operation parameters (neither -selected M1 operation takes one, so M1 ships no directory affordance at all). -Error channels: throw by default; `OpenCodeRequestOptions.NoThrow` per call suppresses only -API-status exceptions; unknown tag / undeclared status / known tag at wrong status → -`UnknownOpenCodeError` with exact tag and raw `JsonElement`; malformed non-2xx JSON → null -typed error + exact raw body; unexpected/malformed 2xx → `OpenCodeTransportException`; -`OperationCanceledException` always passes through. Pipeline signature: -`Task ExecuteAsync(HttpMethod, string route, ResponseAdapter, -OpenCodeRequestOptions?, CancellationToken) where TResponse : OpenCodeResponse` — buffer the -JSON body, synchronous adapter maps `(status, rawBody)`, pipeline applies throw-vs-NoThrow -after adaptation. - -### Task 3 — generated callable surface and ownership - -One file per public type (`OpenCodeClient`, `SessionsClient`, `SessionClient`, both -responses, `OpenCodeRoutes`; adapters under `Internal/ResponseAdapters/`); -`OperationMethodEmitter` returns members to `ClientEmitter` (no partial files); virtual -members + protected mock constructors (unoverridden member → instructive -`InvalidOperationException`, never NRE); guarded payload getters and `PrintMembers`; writer -ownership extension with unmanifested-overwrite refusal + provenance-header requirement + -case-insensitive collision checks; CS1591 gate on; Linux CI `generate --verify` step; -exception constructors need deliberate CA1032 treatment before the API baseline. +requires an explicit request. Run gate chains with `set -o pipefail` — a `| tail` pipe +otherwise swallows a red gate's exit code. + +## Landed This Session (beyond the plan checkboxes) + +- **Error maps derived from the pin:** health = 400 `InvalidRequestError`, 401 + `UnauthorizedError`; message adds 404 as inline `anyOf(SessionNotFoundError, + MessageNotFoundError)`. All `_tag`-marked Effect errors; pinned binder tests assert the maps. +- **Mechanical envelope payload names (maintainer-sealed):** payload = operation subject + (`Health`, `Message`); `envelopePayloadNames` is override-only for response-spine collisions + (reserved: `Error`, `IsError`, `RawBody`, `Status`, and the response type name); a collision + fails generation until an override lands. The required-per-operation validation is retired. +- **`OperationPlanBinder`** (`tools/.../Binding/`): consumed-only `ClientPlan` (Root ×1 + + Collection + Handle roles), `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, + `ErrorMapPlan`; fail-closed walls (GET + JSON + required-string path params + one 200, no + body/query/SSE/websocket/wildcard, non-2xx must be ≥400); `BindingErrorCategory.Operation`; + name/response/client/route-member collision checks; template-ordered parameters; the + synthetic same-shape gadget test proves zero operation/group/client-name branches. +- **Route naming on the plan:** `RouteContainerName` = clientName ?? Pascal(group); + `RouteMemberName` = verb+subject with no group fold (`Health.Get`, `Sessions.GetMessage`). + Maintainer approved the `OpenCodeRoutes` shape: nested static classes, parameterless ops as + `const`, parameterized ops as `Template` const + escaped `(...)` builder. +- **Hand-written core (Task 2 complete):** spine (`ErrorBehavior`, options, `OpenCodeResponse`, + exception spine), `IEnvironmentProvider`/`SystemEnvironmentProvider`, + `EndpointPolicy.Normalize` (absolute HTTP(S), no query/fragment/userinfo, prefix preserved, + returns base string without trailing slash), `UserAgentPolicy` (maintainer-sealed: strip + `+metadata`; missing/unparsable → version token omitted, never assembly-version fallback, + never a construction failure), `Pipeline` (per-request Basic `opencode` decoration, explicit + password before one-time `OPENCODE_SERVER_PASSWORD` read at construction, single + `SendAsync(ResponseHeadersRead)`, buffered body → synchronous `ResponseAdapter`, + throw-vs-NoThrow after adaptation, `OpenCodeTransportException` wrap, OCE passthrough, + owned-client disposal, `ObjectDisposedException` after dispose). +- **Multi-TFM traps solved once:** implicit usings lack `System.Net.Http` below net6 → + conditional `` for net472/ns2.0 in the SDK csproj; `InternalsVisibleTo` + + source-only Polyfill → `PolyUseEmbeddedAttribute=true` (research doc 14 updated); + `DynamicProxyGenAssembly2` friend for NSubstitute over internal seams; S1694 arbitration on + `ResponseAdapter` (abstract base will carry shared mapping helpers; DIM unavailable on + net472/ns2.0). + +## Remaining Work + +### Task 3b — emitters (next) + +Sequenced so gates stay green per commit: +1. **Commit E:** `RoutesEmitter`, `EnvelopeEmitter`, `ResponseAdapterEmitter`, + `ClientEmitter`+`OperationMethodEmitter` (members returned, no partials), synthetic + `EmitterPlanFixture` client plans, micro-snapshots, adapter-base shared helpers + (`ReadBarePayload`/`ReadDataPayload`/`ReadTolerantError` on `ResponseAdapter` — + hand-written, tag-set param `IReadOnlyCollection?`, null = undeclared status; + FrozenSet is net8+, use `string[]`). Do NOT wire `SourceEmitter` yet — generate must not + emit new files before the writer admits their roots. +2. **Commit F (Task 3c):** writer ownership extension (root files + `Internal/ResponseAdapters/`), + unmanifested-overwrite refusal, provenance-header requirement, case-insensitive collisions. +3. **Commit G:** wire `SourceEmitter`, CA1034 arbitration for generated `OpenCodeRoutes.cs`, + CS1591 gate, regenerate (committed surface appears), Linux CI `generate --verify` step. + +Emitted shapes settled: envelopes = sealed records over `OpenCodeResponse`, backing-field +guarded payload getter, explicit public parameterless ctor (any explicit ctor kills the +implicit one), internal `[SetsRequiredMembers]` error ctor (`Status/IsError/Error/RawBody`), +`protected override bool PrintMembers` appending the payload only when present. Adapters = +`Internal/ResponseAdapters/`, sealed, private ctor + `Instance`, status-switch: 200 → payload +read through `OpenCodeJsonContext.Default.`, else error ctor with per-status +`string[]` tag sets. Clients = unsealed, virtual members, protected mock ctor, guarded +internal `Pipeline` accessor throwing instructive `InvalidOperationException`, root owns +`Pipeline.Create` + `SystemEnvironmentProvider` + CA1063 Dispose pattern, handle holds +immutable applied parameter, method bodies = guard + one `Pipeline.ExecuteAsync` delegation +via `OpenCodeRoutes`. Aggregate compile probe must add the hand-written SDK sources (copy +`src/OpenCode.Sdk/**/*.cs` into test fixtures, exclude paths that collide with freshly emitted +sources) — the probe references only BCL assemblies and would otherwise miss the spine. ### Task 4 — contracts and M1 closure -Contract matrix through a recording `HttpMessageHandler` (health + message success/error -statuses under default and `NoThrow`; error skew: unknown tag, undeclared status, known tag at -wrong status, malformed body; envelope guards incl. `ToString`; route escaping; endpoint -joining; auth resolution; User-Agent token; cancellation; transport failures; owned/BYO -lifetime; mock seam). Pin `PublicApiGenerator` 11.5.4 + `Verify.TUnit`, review one API -baseline. Keep `.generation-incomplete`; pack must fail only because breadth is pending. -net472 runs on Windows CI as the ns2.0 proxy; `App.net472.config` never flows into the -package. +Unchanged from the plan: recording-handler contract matrix, error-skew tests +(unknown tag / undeclared status / known tag at wrong status → `UnknownOpenCodeError`; +malformed non-2xx → null typed error + exact raw body), guarded payload/`ToString`, route +escaping, endpoint joining, auth resolution, User-Agent, cancellation, lifetime, mock seam; +`PublicApiGenerator` 11.5.4 + `Verify.TUnit` baseline review; pack fails only on +`.generation-incomplete`; real `opencode2 serve` demo from `.scratchpad/`; move ROADMAP to M2; +delete this handoff. ## Real-Server Demonstration -`opencode2` v0.0.0-next-17403 is installed on this machine -(`~/.nvm/versions/node/v24.18.0/bin/opencode2`, npm `@opencode-ai/cli@next`). Verified live -this session: install starts no server (`opencode2 service status` → stopped); -`opencode2 serve --hostname 127.0.0.1 --port

` prints the endpoint and a generated -password; unauthenticated `/api/health` → 401; Basic `opencode:` → 200 -`{"healthy":true,"version":"…","pid":…}`; `/api/session?limit=2` returns real sessions in the -`{data}` envelope. For the demo: start a password-enabled `opencode2 serve`, obtain IDs via -the pinned surface's session-list and message-list operations, call only the two generated -methods from a temporary `.scratchpad/` console, print the health payload + concrete message -type + message `Id`, paste commands and output into the PR, and record the version skew -between the pin (`a6a712a`) and the installed `next` build as visible demo evidence. +`opencode2` v0.0.0-next-17403 (`~/.nvm/versions/node/v24.18.0/bin/opencode2`). Verified: +`serve` prints endpoint + generated password; unauthenticated `/api/health` → 401; Basic +`opencode:` → 200 `{healthy, version, pid}`; sessions come back in `{data}`. +Record the pin (`a6a712a`) vs installed-build skew as demo evidence. ## Final Gates @@ -212,22 +130,9 @@ The first six must succeed; pack must fail only on the `.generation-incomplete` ## Stop Conditions -Stop and involve the maintainer if: the Binder needs Microsoft.OpenApi or raw JSON for a fact -SpecIR claims to provide; an emitter needs an operation ID, wire group, or concrete -client-name branch; a selected shape exceeds the declared M1 capability rather than failing -closed; a **structural** union enters the selected closure (nested *marked* unions are now -supported — one level only); generated ownership cannot distinguish hand-written files; a -temporary public or transport API becomes necessary; any locked TFM fails to compile; partial -generation packs successfully; or retry/telemetry/DI/launcher/SSE/breadth becomes necessary -merely to call the two M1 operations. - -## Recommended First Actions - -1. Read the onboarding list above; inspect `git log origin/master..HEAD` and confirm the - worktree is clean. -2. Run the first six gates once to confirm the green baseline before changing anything. -3. Derive both operations' v2 error maps from the pin (the open Task 0 box) — this feeds - directly into `ErrorMapPlan`. -4. Red-test `handleParameter` and the two pinned operation plans, plus the synthetic - same-shape operation, before implementing `OperationPlanBinder`. -5. Continue in the plan's task order; delete this handoff when Arc B ships. +Unchanged: Binder needing Microsoft.OpenApi/raw JSON; an emitter needing an operation ID, +wire group, or concrete client-name branch; selected shape beyond M1 capability; structural +union in the closure; ownership unable to distinguish hand-written files; temporary +public/transport API; a locked TFM failing to compile; partial generation packing; or +retry/telemetry/DI/launcher/SSE/breadth creeping in. Public-API taste decisions go to the +maintainer. From 367f8ff60a2626ef031c8bfe3cc035aa273ce8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 16:58:58 +0300 Subject: [PATCH 14/28] feat(generator): emit the callable client surface RoutesEmitter renders nested route containers (const for parameterless operations, template const plus escaped builder otherwise); EnvelopeEmitter renders sealed response records with a guarded payload getter, the internal SetsRequiredMembers error constructor carrying the SDK's single null-forgiveness, and a payload-skipping PrintMembers; ResponseAdapterEmitter renders status-switched adapters over the hand-written tolerant-reading base (bare and data payload readers, tag-set downgrade to the unknown carrier); ClientEmitter plus OperationMethodEmitter render virtual members, protected mock constructors behind the MockSeam failure, root ownership of Pipeline.Create and Dispose, immutable handle state, and one pipeline delegation per operation. The aggregate probe now compiles fresh emitter output together with the hand-written core sources, replaying the production compilation unit; SourceEmitter stays unwired until the writer admits the new roots. --- src/OpenCode.Sdk/Internal/MockSeam.cs | 14 + src/OpenCode.Sdk/Internal/ResponseAdapter.cs | 91 +++++ src/OpenCode.Sdk/OpenCode.Sdk.csproj | 3 +- .../ResponseAdapterTests.cs | 114 ++++++ .../CallableSurfaceCompilationTests.cs | 23 ++ .../Generator/Emission/ClientEmitterTests.cs | 15 + .../Emission/EnvelopeEmitterTests.cs | 15 + .../Emission/ResponseAdapterEmitterTests.cs | 15 + .../Generator/Emission/RoutesEmitterTests.cs | 15 + .../OpenCode.Sdk.Tools.Tests.csproj | 6 + ...ce_Virtual_Delegating_Clients.verified.txt | 204 +++++++++++ ...ce_Guarded_Response_Envelopes.verified.txt | 185 ++++++++++ ...roduce_Status_Mapped_Adapters.verified.txt | 119 +++++++ ...roduce_Escaped_Route_Builders.verified.txt | 50 +++ .../Support/EmitterPlanFixture.cs | 161 ++++++++- .../Support/GeneratedSourceCompiler.cs | 61 +++- .../Generator/Emission/ClientEmitter.cs | 336 ++++++++++++++++++ .../Generator/Emission/DocumentedException.cs | 3 + .../Generator/Emission/DocumentedParameter.cs | 3 + .../Generator/Emission/EmissionSyntax.cs | 28 ++ .../Generator/Emission/EnvelopeEmitter.cs | 189 ++++++++++ .../Emission/OperationMethodEmitter.cs | 122 +++++++ .../Emission/ResponseAdapterEmitter.cs | 163 +++++++++ .../Generator/Emission/RoutesEmitter.cs | 139 ++++++++ 24 files changed, 2068 insertions(+), 6 deletions(-) create mode 100644 src/OpenCode.Sdk/Internal/MockSeam.cs create mode 100644 tests/OpenCode.Sdk.Tests/ResponseAdapterTests.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ClientEmitterTests.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/EnvelopeEmitterTests.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ResponseAdapterEmitterTests.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/RoutesEmitterTests.cs create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Snapshots/ClientEmitterTests.Emit_Should_Produce_Virtual_Delegating_Clients.verified.txt create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Snapshots/EnvelopeEmitterTests.Emit_Should_Produce_Guarded_Response_Envelopes.verified.txt create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Snapshots/ResponseAdapterEmitterTests.Emit_Should_Produce_Status_Mapped_Adapters.verified.txt create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Snapshots/RoutesEmitterTests.Emit_Should_Produce_Escaped_Route_Builders.verified.txt create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Emission/ClientEmitter.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Emission/DocumentedException.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Emission/DocumentedParameter.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Emission/EnvelopeEmitter.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Emission/OperationMethodEmitter.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Emission/RoutesEmitter.cs diff --git a/src/OpenCode.Sdk/Internal/MockSeam.cs b/src/OpenCode.Sdk/Internal/MockSeam.cs new file mode 100644 index 00000000..f6e47a5a --- /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/ResponseAdapter.cs b/src/OpenCode.Sdk/Internal/ResponseAdapter.cs index de42a07b..df82ade8 100644 --- a/src/OpenCode.Sdk/Internal/ResponseAdapter.cs +++ b/src/OpenCode.Sdk/Internal/ResponseAdapter.cs @@ -1,3 +1,8 @@ +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. @@ -10,4 +15,90 @@ internal abstract class ResponseAdapter /// 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); + } + } + + /// + /// 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/OpenCode.Sdk.csproj b/src/OpenCode.Sdk/OpenCode.Sdk.csproj index d2418518..5734826f 100644 --- a/src/OpenCode.Sdk/OpenCode.Sdk.csproj +++ b/src/OpenCode.Sdk/OpenCode.Sdk.csproj @@ -20,8 +20,9 @@ - + + diff --git a/tests/OpenCode.Sdk.Tests/ResponseAdapterTests.cs b/tests/OpenCode.Sdk.Tests/ResponseAdapterTests.cs new file mode 100644 index 00000000..749315bb --- /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.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs new file mode 100644 index 00000000..1d2e3d52 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs @@ -0,0 +1,23 @@ +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 plan = EmitterPlanFixture.Create(); + var sources = new List(); + sources.AddRange(SourceEmitter.Emit(plan)); + 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 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 00000000..02e9b7fb --- /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 00000000..367e609c --- /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/ResponseAdapterEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/ResponseAdapterEmitterTests.cs new file mode 100644 index 00000000..3d347d89 --- /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 00000000..94a65cf1 --- /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/OpenCode.Sdk.Tools.Tests.csproj b/tests/OpenCode.Sdk.Tools.Tests/OpenCode.Sdk.Tools.Tests.csproj index 78646c8c..9a32364d 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/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 00000000..2029cfe9 --- /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 00000000..c816a12c --- /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/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 00000000..d13f01d7 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/ResponseAdapterEmitterTests.Emit_Should_Produce_Status_Mapped_Adapters.verified.txt @@ -0,0 +1,119 @@ +// 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) + }, + 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) + }, + 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) + }, + _ => 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 00000000..31b57e53 --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/RoutesEmitterTests.Emit_Should_Produce_Escaped_Route_Builders.verified.txt @@ -0,0 +1,50 @@ +// 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.ThrowIfNullOrEmpty(widgetId); + ArgumentException.ThrowIfNullOrEmpty(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/Support/EmitterPlanFixture.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/EmitterPlanFixture.cs index 58ffeb1f..8b63dd0e 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 = @@ -41,11 +42,169 @@ public static EmitPlan Create() "UnknownOpenCodeError", ], }, - Clients = [], PendingOperations = [], }; } + 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 281103a6..4d767db0 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/GeneratedSourceCompiler.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/GeneratedSourceCompiler.cs @@ -17,11 +17,64 @@ internal static class GeneratedSourceCompiler public static Diagnostic[] Compile(IReadOnlyList sources) { ArgumentNullException.ThrowIfNull(sources); + + return Compile(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/tools/OpenCode.Sdk.Tools/Generator/Emission/ClientEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/ClientEmitter.cs new file mode 100644 index 00000000..36ee02ad --- /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 00000000..6d8125f5 --- /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 00000000..74680f2a --- /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 a31c322f..c297f818 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs @@ -74,6 +74,34 @@ public static SyntaxTriviaList Documentation(string summary) 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 00000000..62d4d753 --- /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/OperationMethodEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/OperationMethodEmitter.cs new file mode 100644 index 00000000..976cb45d --- /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/ResponseAdapterEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs new file mode 100644 index 00000000..f1a3c81f --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs @@ -0,0 +1,163 @@ +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)), + }; + 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 00000000..af64aaa3 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/RoutesEmitter.cs @@ -0,0 +1,139 @@ +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(EmissionSyntax.ArgumentNullOrEmptyGuard(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); + } + + /// 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)); +} From bedb6be6425b87f5cac8c2e5762396522ba3ab1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 17:07:41 +0300 Subject: [PATCH 15/28] feat(generator): extend writer ownership to the callable surface The writer now owns root-level generated files and the response-adapter subtree alongside models and serialization. Three fail-closed rules join the manifest-diff cleanup: a new path that already exists on disk is refused as unmanifested, manifest entries are only overwritten or deleted while they still carry the exact provenance header (now shared between emission and the writer, and validated on incoming sources so a headerless emit cannot deadlock the next run), and case-only collisions stay refused. CS1591 becomes an error outside tests, CI gains the Linux generate --verify step, and the generated route surface's nested containers get their narrow CA1034 arbitration. --- .editorconfig | 12 +- .github/workflows/ci.yml | 5 + .../Output/GenerationWriterTests.cs | 121 +++++++++++++++++- .../Support/GenerationTestData.cs | 5 +- .../OpenCode.Sdk.Tools.Tests/ToolAppTests.cs | 2 +- .../Generator/Emission/EmissionSyntax.cs | 4 +- .../Emission/GenerationProvenance.cs | 22 ++++ .../Output/GenerationWriter.cs | 48 ++++++- 8 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Emission/GenerationProvenance.cs diff --git a/.editorconfig b/.editorconfig index 73f87b3b..6cf65c4e 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 @@ -788,3 +791,10 @@ dotnet_diagnostic.CA1720.severity = none # 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 aec648eb..c8b5d2ae 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/tests/OpenCode.Sdk.Tools.Tests/Output/GenerationWriterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Output/GenerationWriterTests.cs index 7c75abff..cc4abd83 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] diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/GenerationTestData.cs index 7678261d..a603d7ba 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": { @@ -52,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/ToolAppTests.cs b/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs index 35bab9c1..fe220969 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs @@ -145,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/Generator/Emission/EmissionSyntax.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs index c297f818..493f54ee 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/EmissionSyntax.cs @@ -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); } 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 00000000..571e2db5 --- /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/Output/GenerationWriter.cs b/tools/OpenCode.Sdk.Tools/Output/GenerationWriter.cs index 6c678e51..f7eeee55 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."); @@ -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) From f35824952e3bca41b3ff36fd27cd322dc9f50db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 17:12:36 +0300 Subject: [PATCH 16/28] feat(generator): wire the callable surface into generation SourceEmitter now renders routes, envelopes, adapters, and clients whenever the plan carries clients, and the pinned profile's callable surface lands as committed source: OpenCodeClient.GetHealthAsync, Sessions.GetSessionClient, SessionClient.GetMessageAsync, both guarded envelopes, the OpenCodeRoutes containers, and the two response adapters. The aggregate compile probes run through SourceEmitter and the hand-written core. --- .../2026-08-13-m1-arc-b-callable-client.md | 10 +-- src/OpenCode.Sdk/.generated-manifest.json | 10 ++- src/OpenCode.Sdk/HealthResponse.cs | 59 ++++++++++++ .../ResponseAdapters/HealthResponseAdapter.cs | 40 +++++++++ .../SessionMessageResponseAdapter.cs | 42 +++++++++ src/OpenCode.Sdk/OpenCodeClient.cs | 90 +++++++++++++++++++ src/OpenCode.Sdk/OpenCodeRoutes.cs | 42 +++++++++ src/OpenCode.Sdk/SessionClient.cs | 46 ++++++++++ src/OpenCode.Sdk/SessionMessageResponse.cs | 59 ++++++++++++ src/OpenCode.Sdk/SessionsClient.cs | 37 ++++++++ .../CallableSurfaceCompilationTests.cs | 8 +- .../Generator/Emission/SourceEmitterTests.cs | 12 +-- .../Support/GeneratedSourceCompiler.cs | 7 -- .../Generator/Emission/SourceEmitter.cs | 7 ++ 14 files changed, 438 insertions(+), 31 deletions(-) create mode 100644 src/OpenCode.Sdk/HealthResponse.cs create mode 100644 src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs create mode 100644 src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageResponseAdapter.cs create mode 100644 src/OpenCode.Sdk/OpenCodeClient.cs create mode 100644 src/OpenCode.Sdk/OpenCodeRoutes.cs create mode 100644 src/OpenCode.Sdk/SessionClient.cs create mode 100644 src/OpenCode.Sdk/SessionMessageResponse.cs create mode 100644 src/OpenCode.Sdk/SessionsClient.cs 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 index 3d4135f4..9b37d5b4 100644 --- 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 @@ -127,18 +127,18 @@ and committed generated models. **Areas:** new emitters, `SourceEmitter`, `GenerationWriter`, analyzer config, CI, and tests. -- [ ] Emit one file per public type: `OpenCodeClient`, `SessionsClient`, `SessionClient`, both +- [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. -- [ ] Emit virtual client members/protected mock constructors, immutable handle state, escaped +- [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. -- [ ] Extend ownership to root generated files and the adapter subtree. Refuse unmanifested +- [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. -- [ ] Add one micro-snapshot per emitter family, aggregate generated-source compilation, and +- [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. -- [ ] Enable the decided CS1591 gate, document hand-written public members, add only necessary +- [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 diff --git a/src/OpenCode.Sdk/.generated-manifest.json b/src/OpenCode.Sdk/.generated-manifest.json index 1f6c7e8f..e68c919a 100644 --- a/src/OpenCode.Sdk/.generated-manifest.json +++ b/src/OpenCode.Sdk/.generated-manifest.json @@ -1,5 +1,8 @@ { "files": [ + "HealthResponse.cs", + "Internal/ResponseAdapters/HealthResponseAdapter.cs", + "Internal/ResponseAdapters/SessionMessageResponseAdapter.cs", "Internal/Serialization/OpenCodeErrorJsonConverter.cs", "Internal/Serialization/OpenCodeJsonContext.cs", "Internal/Serialization/OptionalCollectionInput.cs", @@ -82,6 +85,11 @@ "Models/UnknownSessionMessageCompaction.cs", "Models/UnknownSessionMessageInfo.cs", "Models/UnknownSessionMessageToolState.cs", - "Models/UnknownToolContent.cs" + "Models/UnknownToolContent.cs", + "OpenCodeClient.cs", + "OpenCodeRoutes.cs", + "SessionClient.cs", + "SessionMessageResponse.cs", + "SessionsClient.cs" ] } diff --git a/src/OpenCode.Sdk/HealthResponse.cs b/src/OpenCode.Sdk/HealthResponse.cs new file mode 100644 index 00000000..8cdbaa27 --- /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/ResponseAdapters/HealthResponseAdapter.cs b/src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs new file mode 100644 index 00000000..f02f16ae --- /dev/null +++ b/src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs @@ -0,0 +1,40 @@ +// 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) + }, + 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 00000000..fc71107b --- /dev/null +++ b/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageResponseAdapter.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 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) + }, + 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/OpenCodeClient.cs b/src/OpenCode.Sdk/OpenCodeClient.cs new file mode 100644 index 00000000..90fc48d1 --- /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/OpenCodeRoutes.cs b/src/OpenCode.Sdk/OpenCodeRoutes.cs new file mode 100644 index 00000000..10c0e9c6 --- /dev/null +++ b/src/OpenCode.Sdk/OpenCodeRoutes.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. +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.ThrowIfNullOrEmpty(sessionId); + ArgumentException.ThrowIfNullOrEmpty(messageId); + return "/api/session/" + Uri.EscapeDataString(sessionId) + "/message/" + Uri.EscapeDataString(messageId); + } + } +} diff --git a/src/OpenCode.Sdk/SessionClient.cs b/src/OpenCode.Sdk/SessionClient.cs new file mode 100644 index 00000000..0a148ca6 --- /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 00000000..aa3824d3 --- /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 00000000..d24afb4e --- /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.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs index 1d2e3d52..acaaf492 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/CallableSurfaceCompilationTests.cs @@ -8,13 +8,7 @@ public sealed class CallableSurfaceCompilationTests [Test] public async Task Emit_Should_Produce_A_Compilable_Callable_Surface() { - var plan = EmitterPlanFixture.Create(); - var sources = new List(); - sources.AddRange(SourceEmitter.Emit(plan)); - 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 sources = SourceEmitter.Emit(EmitterPlanFixture.Create()); var diagnostics = await GeneratedSourceCompiler.CompileWithSdkCoreAsync(sources); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/SourceEmitterTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/SourceEmitterTests.cs index 714f7411..66d674de 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/Support/GeneratedSourceCompiler.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/GeneratedSourceCompiler.cs index 4d767db0..68ae7304 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/GeneratedSourceCompiler.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/GeneratedSourceCompiler.cs @@ -14,13 +14,6 @@ internal static class GeneratedSourceCompiler private static readonly PortableExecutableReference[] References = CreateReferences(); private static readonly AnalyzerFileReference SourceGenerator = CreateSourceGeneratorReference(); - public static Diagnostic[] Compile(IReadOnlyList sources) - { - ArgumentNullException.ThrowIfNull(sources); - - return Compile(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. diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs index f7d7ffb0..b2e1c8fe 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/SourceEmitter.cs @@ -12,6 +12,13 @@ public static IReadOnlyList Emit(EmitPlan 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) From 52a78103e1eb7ca9401b1cf031dce7034785b985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 17:18:27 +0300 Subject: [PATCH 17/28] test(sdk): cover the callable surface with a contract matrix Recording-handler tests exercise the real generated clients end to end: typed health and message successes (data envelope unwrap, escaped routes), both declared 404 error types, default throw versus NoThrow, unknown-tag / undeclared-status / wrong-status downgrades to the unknown carrier with the exact tag and raw payload, malformed error bodies preserving the raw body with a null typed error on both channels, malformed 2xx as a transport failure, guarded payload access and printing on the error path, the instructive mock-seam failure, bound handles going unusable on root disposal, and the BYO endpoint requirement. --- .../2026-08-13-m1-arc-b-callable-client.md | 6 +- .../OpenCodeClientContractTests.cs | 267 ++++++++++++++++++ 2 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs 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 index 9b37d5b4..baa2462f 100644 --- 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 @@ -143,13 +143,13 @@ and committed generated models. ## Task 4: Contracts And M1 Closure -- [ ] Test real generated clients through a recording `HttpMessageHandler`: health 200/400/401; +- [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. -- [ ] Assert an undeclared status and a known tag returned at the wrong declared status both +- [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. -- [ ] For malformed non-2xx JSON, assert `Error` is null and exact `RawBody` is retained on both +- [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. - [ ] 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 diff --git a/tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs new file mode 100644 index 00000000..bfe16375 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs @@ -0,0 +1,267 @@ +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 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), + }; +} From ca128a00e276c373179b80683e8fd45281288a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 17:24:24 +0300 Subject: [PATCH 18/28] test(sdk): lock the reviewed public API baseline PublicApiGenerator 11.5.4 renders the SDK's public surface into one TFM-shared Verify baseline: the thirteen root types plus the generated model closure, with no internal type leaking. Packing still fails only on the generation-incomplete marker. --- Directory.Packages.props | 1 + .../2026-08-13-m1-arc-b-callable-client.md | 4 +- tests/OpenCode.Sdk.Tests/ModuleInitializer.cs | 12 + .../OpenCode.Sdk.Tests.csproj | 2 + .../PublicApiBaselineTests.cs | 21 + .../Snapshots/PublicApi.verified.txt | 928 ++++++++++++++++++ 6 files changed, 966 insertions(+), 2 deletions(-) create mode 100644 tests/OpenCode.Sdk.Tests/ModuleInitializer.cs create mode 100644 tests/OpenCode.Sdk.Tests/PublicApiBaselineTests.cs create mode 100644 tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt diff --git a/Directory.Packages.props b/Directory.Packages.props index fc4a1e41..80104f6f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,6 +41,7 @@ + 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 index baa2462f..b294565f 100644 --- 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 @@ -151,10 +151,10 @@ and committed generated models. 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. -- [ ] Pin `PublicApiGenerator` 11.5.4 centrally, add it plus `Verify.TUnit` to +- [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. -- [ ] Run `generate`, clean `generate --verify`, Release build, supported test legs, format, +- [x] Run `generate`, clean `generate --verify`, Release build, supported test legs, format, Slopwatch, and intentional pack refusal. - [ ] 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 diff --git a/tests/OpenCode.Sdk.Tests/ModuleInitializer.cs b/tests/OpenCode.Sdk.Tests/ModuleInitializer.cs new file mode 100644 index 00000000..22f21765 --- /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 935638c3..663673fc 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 diff --git a/tests/OpenCode.Sdk.Tests/PublicApiBaselineTests.cs b/tests/OpenCode.Sdk.Tests/PublicApiBaselineTests.cs new file mode 100644 index 00000000..73c04ebc --- /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/Snapshots/PublicApi.verified.txt b/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt new file mode 100644 index 00000000..2690f098 --- /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 From 257384a17993df608c6b8d6286c01da923448b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 17:30:46 +0300 Subject: [PATCH 19/28] docs(roadmap): close M1 after the live walking-skeleton demonstration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both generated operations ran against a real opencode2 serve (v0.0.0-next-17403; pin a6a712a — deliberate snapshot skew): GetHealthAsync returned the typed ServiceHealth payload and GetMessageAsync returned a typed SessionMessageAssistant through the bound session handle. The Arc B plan's final box is checked, the roadmap moves to M2, and the consumed session handover is deleted. --- docs/ROADMAP.md | 25 ++-- .../handover-prompts/HANDOFF-2026-08-13-2.md | 138 ------------------ .../2026-08-13-m1-arc-b-callable-client.md | 2 +- 3 files changed, 15 insertions(+), 150 deletions(-) delete mode 100644 docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1fecee73..dfc5b6e2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -7,18 +7,21 @@ Evergreen rules and locked decisions live in `../AGENTS.md`; decision records in ## 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. +**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`). -Direction sealed and executed 2026-08-13: the SDK targets the **v2 protocol surface only** -(ADR-0005; platform evidence: research doc 15), and `spec/` pins v2-branch commit `a6a712a` -(Arc B Task 0 — landed). The active work is the rest of M1 Arc B: operation binding, the -minimal transport core, and the callable client with typed errors and `NoThrow`; see the -just-in-time -[M1 Arc B callable-client plan](superpowers/plans/2026-08-13-m1-arc-b-callable-client.md). +The next work is M2 — the first breadth batch, planned when it starts. ## Milestones diff --git a/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md b/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md deleted file mode 100644 index 67454952..00000000 --- a/docs/agents/handover-prompts/HANDOFF-2026-08-13-2.md +++ /dev/null @@ -1,138 +0,0 @@ -# M1 Arc B Handoff — Tasks 3b–4 - -Date: 2026-08-13 - -Consume this document against live Git and delete it when Arc B ships. It records unfinished -session state, not evergreen architecture. - -## Onboarding Reading Order - -1. `AGENTS.md` — locked decisions (ADR-0005 targets the v2 protocol surface only). -2. ADRs 0003/0004/0005/0007/0008/0009 — the binding design spine. -3. `docs/superpowers/plans/2026-08-13-m1-arc-b-callable-client.md` — the canonical execution - plan. Tasks 0–2 are fully checked; Task 3 is in flight (route naming landed); everything - unchecked is this handoff's work. -4. `docs/research/15-opencode-v2-platform.md` — the canonical v2 platform picture. -5. `docs/engineering/coding-style.md` and `docs/engineering/testing-style.md` — before writing - any code. - -## Git And Workspace State - -| Item | State | -|---|---| -| Workspace | `/home/deniz/src/opencode-sdk-dotnet` | -| Branch | `feature/m1-callable-client` | -| HEAD | `aa7e351 feat(generator): plan route container and member names` | -| Ahead of `origin/master` | 13 commits, no remote branch, no pushes | -| Gates at HEAD | all six green (441 tests; net8/9/10 SDK legs, tools) | - -The maintainer authorized implementation commits inside this development loop; pushing still -requires an explicit request. Run gate chains with `set -o pipefail` — a `| tail` pipe -otherwise swallows a red gate's exit code. - -## Landed This Session (beyond the plan checkboxes) - -- **Error maps derived from the pin:** health = 400 `InvalidRequestError`, 401 - `UnauthorizedError`; message adds 404 as inline `anyOf(SessionNotFoundError, - MessageNotFoundError)`. All `_tag`-marked Effect errors; pinned binder tests assert the maps. -- **Mechanical envelope payload names (maintainer-sealed):** payload = operation subject - (`Health`, `Message`); `envelopePayloadNames` is override-only for response-spine collisions - (reserved: `Error`, `IsError`, `RawBody`, `Status`, and the response type name); a collision - fails generation until an override lands. The required-per-operation validation is retired. -- **`OperationPlanBinder`** (`tools/.../Binding/`): consumed-only `ClientPlan` (Root ×1 + - Collection + Handle roles), `OperationPlan`, `OperationParameterPlan`, `EnvelopePlan`, - `ErrorMapPlan`; fail-closed walls (GET + JSON + required-string path params + one 200, no - body/query/SSE/websocket/wildcard, non-2xx must be ≥400); `BindingErrorCategory.Operation`; - name/response/client/route-member collision checks; template-ordered parameters; the - synthetic same-shape gadget test proves zero operation/group/client-name branches. -- **Route naming on the plan:** `RouteContainerName` = clientName ?? Pascal(group); - `RouteMemberName` = verb+subject with no group fold (`Health.Get`, `Sessions.GetMessage`). - Maintainer approved the `OpenCodeRoutes` shape: nested static classes, parameterless ops as - `const`, parameterized ops as `Template` const + escaped `(...)` builder. -- **Hand-written core (Task 2 complete):** spine (`ErrorBehavior`, options, `OpenCodeResponse`, - exception spine), `IEnvironmentProvider`/`SystemEnvironmentProvider`, - `EndpointPolicy.Normalize` (absolute HTTP(S), no query/fragment/userinfo, prefix preserved, - returns base string without trailing slash), `UserAgentPolicy` (maintainer-sealed: strip - `+metadata`; missing/unparsable → version token omitted, never assembly-version fallback, - never a construction failure), `Pipeline` (per-request Basic `opencode` decoration, explicit - password before one-time `OPENCODE_SERVER_PASSWORD` read at construction, single - `SendAsync(ResponseHeadersRead)`, buffered body → synchronous `ResponseAdapter`, - throw-vs-NoThrow after adaptation, `OpenCodeTransportException` wrap, OCE passthrough, - owned-client disposal, `ObjectDisposedException` after dispose). -- **Multi-TFM traps solved once:** implicit usings lack `System.Net.Http` below net6 → - conditional `` for net472/ns2.0 in the SDK csproj; `InternalsVisibleTo` + - source-only Polyfill → `PolyUseEmbeddedAttribute=true` (research doc 14 updated); - `DynamicProxyGenAssembly2` friend for NSubstitute over internal seams; S1694 arbitration on - `ResponseAdapter` (abstract base will carry shared mapping helpers; DIM unavailable on - net472/ns2.0). - -## Remaining Work - -### Task 3b — emitters (next) - -Sequenced so gates stay green per commit: -1. **Commit E:** `RoutesEmitter`, `EnvelopeEmitter`, `ResponseAdapterEmitter`, - `ClientEmitter`+`OperationMethodEmitter` (members returned, no partials), synthetic - `EmitterPlanFixture` client plans, micro-snapshots, adapter-base shared helpers - (`ReadBarePayload`/`ReadDataPayload`/`ReadTolerantError` on `ResponseAdapter` — - hand-written, tag-set param `IReadOnlyCollection?`, null = undeclared status; - FrozenSet is net8+, use `string[]`). Do NOT wire `SourceEmitter` yet — generate must not - emit new files before the writer admits their roots. -2. **Commit F (Task 3c):** writer ownership extension (root files + `Internal/ResponseAdapters/`), - unmanifested-overwrite refusal, provenance-header requirement, case-insensitive collisions. -3. **Commit G:** wire `SourceEmitter`, CA1034 arbitration for generated `OpenCodeRoutes.cs`, - CS1591 gate, regenerate (committed surface appears), Linux CI `generate --verify` step. - -Emitted shapes settled: envelopes = sealed records over `OpenCodeResponse`, backing-field -guarded payload getter, explicit public parameterless ctor (any explicit ctor kills the -implicit one), internal `[SetsRequiredMembers]` error ctor (`Status/IsError/Error/RawBody`), -`protected override bool PrintMembers` appending the payload only when present. Adapters = -`Internal/ResponseAdapters/`, sealed, private ctor + `Instance`, status-switch: 200 → payload -read through `OpenCodeJsonContext.Default.`, else error ctor with per-status -`string[]` tag sets. Clients = unsealed, virtual members, protected mock ctor, guarded -internal `Pipeline` accessor throwing instructive `InvalidOperationException`, root owns -`Pipeline.Create` + `SystemEnvironmentProvider` + CA1063 Dispose pattern, handle holds -immutable applied parameter, method bodies = guard + one `Pipeline.ExecuteAsync` delegation -via `OpenCodeRoutes`. Aggregate compile probe must add the hand-written SDK sources (copy -`src/OpenCode.Sdk/**/*.cs` into test fixtures, exclude paths that collide with freshly emitted -sources) — the probe references only BCL assemblies and would otherwise miss the spine. - -### Task 4 — contracts and M1 closure - -Unchanged from the plan: recording-handler contract matrix, error-skew tests -(unknown tag / undeclared status / known tag at wrong status → `UnknownOpenCodeError`; -malformed non-2xx → null typed error + exact raw body), guarded payload/`ToString`, route -escaping, endpoint joining, auth resolution, User-Agent, cancellation, lifetime, mock seam; -`PublicApiGenerator` 11.5.4 + `Verify.TUnit` baseline review; pack fails only on -`.generation-incomplete`; real `opencode2 serve` demo from `.scratchpad/`; move ROADMAP to M2; -delete this handoff. - -## Real-Server Demonstration - -`opencode2` v0.0.0-next-17403 (`~/.nvm/versions/node/v24.18.0/bin/opencode2`). Verified: -`serve` prints endpoint + generated password; unauthenticated `/api/health` → 401; Basic -`opencode:` → 200 `{healthy, version, pid}`; sessions come back in `{data}`. -Record the pin (`a6a712a`) vs installed-build skew as demo evidence. - -## Final 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 must succeed; pack must fail only on the `.generation-incomplete` marker. - -## Stop Conditions - -Unchanged: Binder needing Microsoft.OpenApi/raw JSON; an emitter needing an operation ID, -wire group, or concrete client-name branch; selected shape beyond M1 capability; structural -union in the closure; ownership unable to distinguish hand-written files; temporary -public/transport API; a locked TFM failing to compile; partial generation packing; or -retry/telemetry/DI/launcher/SSE/breadth creeping in. Public-API taste decisions go to the -maintainer. 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 index b294565f..243f3227 100644 --- 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 @@ -156,7 +156,7 @@ and committed generated models. `.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. -- [ ] Start a password-enabled real `opencode2 serve` (v2 pre-release channel; record the +- [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, From 3b15a5f700c565b2147fa1985960df0d241b3529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 17:51:14 +0300 Subject: [PATCH 20/28] feat(sandbox): add a debuggable local playground project tests/OpenCode.Sdk.Sandbox is a committed console for driving the SDK against a real opencode2 serve under a debugger. It rides the repository's full analyzer and formatting conventions, reads its configuration from environment variables (launchSettings.json prefills the IDE profile), and exercises the SDK's own OPENCODE_SERVER_PASSWORD fallback instead of passing a password explicitly. The M1 demo code moves here from the scratchpad; the project deliberately stays a flat console until the Extensions package brings a host-based composition root. --- OpenCode.slnx | 1 + .../OpenCode.Sdk.Sandbox.csproj | 13 ++++ tests/OpenCode.Sdk.Sandbox/Program.cs | 68 +++++++++++++++++++ .../Properties/launchSettings.json | 13 ++++ tests/OpenCode.Sdk.Sandbox/README.md | 20 ++++++ 5 files changed, 115 insertions(+) create mode 100644 tests/OpenCode.Sdk.Sandbox/OpenCode.Sdk.Sandbox.csproj create mode 100644 tests/OpenCode.Sdk.Sandbox/Program.cs create mode 100644 tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json create mode 100644 tests/OpenCode.Sdk.Sandbox/README.md diff --git a/OpenCode.slnx b/OpenCode.slnx index 0770c524..c913639e 100644 --- a/OpenCode.slnx +++ b/OpenCode.slnx @@ -16,6 +16,7 @@ + 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 00000000..b03344a1 --- /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 00000000..2e0c1711 --- /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 00000000..1eac3ddc --- /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": "", + "OPENCODE_SANDBOX_SESSION_ID": "", + "OPENCODE_SANDBOX_MESSAGE_ID": "" + } + } + } +} diff --git a/tests/OpenCode.Sdk.Sandbox/README.md b/tests/OpenCode.Sdk.Sandbox/README.md new file mode 100644 index 00000000..400cf6ba --- /dev/null +++ b/tests/OpenCode.Sdk.Sandbox/README.md @@ -0,0 +1,20 @@ +# 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 | + +Start a server (`opencode2 serve --hostname 127.0.0.1 --port 4096` prints its generated +password), fill the profile, and F5. The project deliberately stays a flat console for now; +it grows a Generic Host composition root when the Extensions package (DI) lands. From dcd4f8016e9236175c670d39176692428c46730e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 18:03:56 +0300 Subject: [PATCH 21/28] chore(sandbox): prefill the launch profile and document id discovery The sandbox profile ships with a fixed local password and this machine's known session/message ids; the README shows starting the server with OPENCODE_SERVER_PASSWORD (serve reads the same variable the SDK falls back to) and the curl calls that discover ids through the pinned list operations. --- .../Properties/launchSettings.json | 6 ++-- tests/OpenCode.Sdk.Sandbox/README.md | 31 +++++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json b/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json index 1eac3ddc..80e9c6f2 100644 --- a/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json +++ b/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json @@ -4,9 +4,9 @@ "commandName": "Project", "environmentVariables": { "OPENCODE_SANDBOX_ENDPOINT": "http://127.0.0.1:4096", - "OPENCODE_SERVER_PASSWORD": "", - "OPENCODE_SANDBOX_SESSION_ID": "", - "OPENCODE_SANDBOX_MESSAGE_ID": "" + "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 index 400cf6ba..283baac7 100644 --- a/tests/OpenCode.Sdk.Sandbox/README.md +++ b/tests/OpenCode.Sdk.Sandbox/README.md @@ -15,6 +15,31 @@ Configuration comes from environment variables, prefilled for the IDE by | `OPENCODE_SANDBOX_SESSION_ID` | Optional; with a message id, fetches one message | | `OPENCODE_SANDBOX_MESSAGE_ID` | Optional; see above | -Start a server (`opencode2 serve --hostname 127.0.0.1 --port 4096` prints its generated -password), fill the profile, and F5. The project deliberately stays a flat console for now; -it grows a Generic Host composition root when the Extensions package (DI) lands. +## 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. From 0c4fbd9bb7f28ca3b1864bc3bc94e9c1f8515faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 18:03:56 +0300 Subject: [PATCH 22/28] docs(research): record the Arc B execution decisions and the post-M1 queue Session 18 captures the three maintainer-sealed decisions (mechanical envelope payload names with an override-only ledger, the strip-and-omit User-Agent version policy, and the nested route-container shape); the new handover queues the ship steps (maintainer-run multi-agent review, push/PR with the demo transcript, first-contact CI watch), the proposed first M2 batch, and the horizon. --- .../handover-prompts/HANDOFF-2026-08-13-3.md | 75 +++++++++++++++++++ docs/research/00-research-log.md | 48 ++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 docs/agents/handover-prompts/HANDOFF-2026-08-13-3.md 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 00000000..a15624bf --- /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 e9535525..b11e84a5 100644 --- a/docs/research/00-research-log.md +++ b/docs/research/00-research-log.md @@ -1609,3 +1609,51 @@ of the v1 `x-opencode-directory` header is decided inside the retarget task. 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. From 52a5e3b1fdf40556ce385985cc710a93b016cedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 19:10:05 +0300 Subject: [PATCH 23/28] test(sdk): reference System.Net.Http on the net472 leg --- tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj b/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj index 663673fc..2743a1cc 100644 --- a/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj +++ b/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj @@ -25,6 +25,17 @@ + + + + + + + + + + + From b1d2e757225543fb84a5529ee1af4eafa0646ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 23:55:45 +0300 Subject: [PATCH 24/28] docs(agents): seal the performance posture --- AGENTS.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 48cc6968..97307157 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 From 4d5084c57d7e6a2abf03db6b17ef3f3932d1d16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Thu, 13 Aug 2026 23:55:45 +0300 Subject: [PATCH 25/28] docs(roadmap): anchor the review queue to the M series --- docs/ROADMAP.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index dfc5b6e2..9220ea5e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -21,6 +21,12 @@ partial-operation marker while breadth is pending. Demonstrated live 2026-08-13 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 @@ -39,11 +45,16 @@ is revisited at each milestone boundary. `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. + 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. + 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 @@ -52,8 +63,10 @@ is revisited at each milestone boundary. 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 From 959c4f0b2e4bf27781383898c5a93ed80dc0583a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Fri, 14 Aug 2026 00:07:25 +0300 Subject: [PATCH 26/28] test(perf): stand up the BenchmarkDotNet suite with P-finding baselines --- Directory.Packages.props | 1 + OpenCode.slnx | 1 + src/OpenCode.Sdk/Properties/AssemblyInfo.cs | 3 + .../Benchmarks/ClientOperationBenchmarks.cs | 72 +++++++++++++++++++ .../UnionDeserializationBenchmarks.cs | 25 +++++++ .../Fixtures/deep-assistant-message.json | 64 +++++++++++++++++ .../OpenCode.Sdk.Performance.Tests.csproj | 26 +++++++ .../OpenCode.Sdk.Performance.Tests/Program.cs | 8 +++ .../Support/BenchmarkFixtures.cs | 31 ++++++++ .../Support/CannedResponseHandler.cs | 19 +++++ 10 files changed, 250 insertions(+) create mode 100644 tests/OpenCode.Sdk.Performance.Tests/Benchmarks/ClientOperationBenchmarks.cs create mode 100644 tests/OpenCode.Sdk.Performance.Tests/Benchmarks/UnionDeserializationBenchmarks.cs create mode 100644 tests/OpenCode.Sdk.Performance.Tests/Fixtures/deep-assistant-message.json create mode 100644 tests/OpenCode.Sdk.Performance.Tests/OpenCode.Sdk.Performance.Tests.csproj create mode 100644 tests/OpenCode.Sdk.Performance.Tests/Program.cs create mode 100644 tests/OpenCode.Sdk.Performance.Tests/Support/BenchmarkFixtures.cs create mode 100644 tests/OpenCode.Sdk.Performance.Tests/Support/CannedResponseHandler.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 80104f6f..769b76b8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,6 +29,7 @@ + diff --git a/OpenCode.slnx b/OpenCode.slnx index c913639e..2254cb7a 100644 --- a/OpenCode.slnx +++ b/OpenCode.slnx @@ -16,6 +16,7 @@ + diff --git a/src/OpenCode.Sdk/Properties/AssemblyInfo.cs b/src/OpenCode.Sdk/Properties/AssemblyInfo.cs index fb330a45..e9c07fea 100644 --- a/src/OpenCode.Sdk/Properties/AssemblyInfo.cs +++ b/src/OpenCode.Sdk/Properties/AssemblyInfo.cs @@ -2,5 +2,8 @@ [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/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/ClientOperationBenchmarks.cs b/tests/OpenCode.Sdk.Performance.Tests/Benchmarks/ClientOperationBenchmarks.cs new file mode 100644 index 00000000..ed04b343 --- /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 00000000..2221606d --- /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 00000000..a15ed7e2 --- /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 00000000..9399e597 --- /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 00000000..631f43b7 --- /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 00000000..ff15e8c0 --- /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 00000000..cbf5e7a0 --- /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 }); + } +} From 082b58c3b984dc81b2184202b470f9dbe2abca67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Fri, 14 Aug 2026 00:13:16 +0300 Subject: [PATCH 27/28] fix(sdk): harden pipeline failure channels against BCL escapes --- src/OpenCode.Sdk/Internal/Pipeline.cs | 20 ++++++--- tests/OpenCode.Sdk.Tests/PipelineTests.cs | 55 +++++++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/OpenCode.Sdk/Internal/Pipeline.cs b/src/OpenCode.Sdk/Internal/Pipeline.cs index 82c522f5..177806a6 100644 --- a/src/OpenCode.Sdk/Internal/Pipeline.cs +++ b/src/OpenCode.Sdk/Internal/Pipeline.cs @@ -82,6 +82,13 @@ public async Task ExecuteAsync(HttpMethod method, string r 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))) @@ -93,7 +100,7 @@ public async Task ExecuteAsync(HttpMethod method, string r } var adapted = adapter.Adapt(status, rawBody); - return adapted.IsError && (options?.ErrorBehavior ?? ErrorBehavior.Default) is ErrorBehavior.Default + return adapted.IsError && errorBehavior is ErrorBehavior.Default ? throw CreateApiException(adapted) : adapted; } @@ -130,9 +137,10 @@ private async Task SendCoreAsync(HttpRequestMessage request .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); } - catch (HttpRequestException exception) + catch (Exception exception) when (exception is HttpRequestException or ObjectDisposedException) { - // OperationCanceledException deliberately passes through untouched. + // 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); } } @@ -145,9 +153,11 @@ private static async Task ReadBodyAsync(HttpResponseMessage response, Ca ? string.Empty : await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); } - catch (Exception exception) when (exception is HttpRequestException or IOException or ObjectDisposedException) + catch (Exception exception) when (exception is HttpRequestException or IOException or ObjectDisposedException + or InvalidOperationException) { - // OperationCanceledException deliberately passes through untouched. + // 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); } } diff --git a/tests/OpenCode.Sdk.Tests/PipelineTests.cs b/tests/OpenCode.Sdk.Tests/PipelineTests.cs index 5f021a4b..3e20177e 100644 --- a/tests/OpenCode.Sdk.Tests/PipelineTests.cs +++ b/tests/OpenCode.Sdk.Tests/PipelineTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http.Headers; using System.Reflection; using System.Text; using NSubstitute; @@ -201,6 +202,60 @@ public async Task ExecuteAsync_Should_Wrap_Network_Failures_As_Transport_Failure 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() { From 1de4d78639a90aa1a6a258fb4e8ce99fbbc6f4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Fri, 14 Aug 2026 00:22:58 +0300 Subject: [PATCH 28/28] fix(generator): harden the emitted surface against the review blocker set --- src/OpenCode.Sdk/Internal/ResponseAdapter.cs | 7 +++ .../ResponseAdapters/HealthResponseAdapter.cs | 1 + .../SessionMessageResponseAdapter.cs | 1 + .../OpenCodeErrorJsonConverter.cs | 7 ++- .../PromptFileSourceJsonConverter.cs | 7 ++- ...ionMessageAssistantContentJsonConverter.cs | 7 ++- .../SessionMessageCompactionJsonConverter.cs | 7 ++- .../SessionMessageInfoJsonConverter.cs | 7 ++- .../SessionMessageToolStateJsonConverter.cs | 7 ++- .../Serialization/ToolContentJsonConverter.cs | 7 ++- src/OpenCode.Sdk/OpenCodeRoutes.cs | 14 ++++- .../Serialization/empty-assistant-marker.json | 1 + .../OpenCodeClientContractTests.cs | 63 +++++++++++++++++++ .../OpenCode.Sdk.Tests/OpenCodeRoutesTests.cs | 34 ++++++++++ .../Serialization/OpenCodeJsonContextTests.cs | 9 +++ ...roduce_Status_Mapped_Adapters.verified.txt | 3 + ...roduce_Escaped_Route_Builders.verified.txt | 14 ++++- ...erant_Marked_Union_Converters.verified.txt | 4 +- .../Emission/ResponseAdapterEmitter.cs | 9 +++ .../Generator/Emission/RoutesEmitter.cs | 31 ++++++++- .../Generator/Emission/UnionEmitter.cs | 19 ++++-- 21 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 tests/OpenCode.Sdk.Tests/Fixtures/Serialization/empty-assistant-marker.json create mode 100644 tests/OpenCode.Sdk.Tests/OpenCodeRoutesTests.cs diff --git a/src/OpenCode.Sdk/Internal/ResponseAdapter.cs b/src/OpenCode.Sdk/Internal/ResponseAdapter.cs index df82ade8..6f882b27 100644 --- a/src/OpenCode.Sdk/Internal/ResponseAdapter.cs +++ b/src/OpenCode.Sdk/Internal/ResponseAdapter.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using OpenCode.Sdk.Internal.Serialization; @@ -65,6 +66,12 @@ protected static TPayload ReadDataPayload(string rawBody, JsonTypeInfo } } + /// 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 diff --git a/src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs b/src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs index f02f16ae..0ae4f92b 100644 --- a/src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs +++ b/src/OpenCode.Sdk/Internal/ResponseAdapters/HealthResponseAdapter.cs @@ -32,6 +32,7 @@ public override HealthResponse Adapt(int status, string rawBody) 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 index fc71107b..2f47d645 100644 --- a/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageResponseAdapter.cs +++ b/src/OpenCode.Sdk/Internal/ResponseAdapters/SessionMessageResponseAdapter.cs @@ -33,6 +33,7 @@ public override SessionMessageResponse Adapt(int status, string rawBody) 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), diff --git a/src/OpenCode.Sdk/Internal/Serialization/OpenCodeErrorJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeErrorJsonConverter.cs index c90778bc..e4f7cc01 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/PromptFileSourceJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/PromptFileSourceJsonConverter.cs index c9c04356..8226602e 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/PromptFileSourceJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/PromptFileSourceJsonConverter.cs @@ -41,7 +41,12 @@ public override PromptFileSource Read(ref Utf8JsonReader reader, Type typeToConv 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 PromptFileSource."); diff --git a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantContentJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageAssistantContentJsonConverter.cs index aca524b0..272c6b75 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 index d0031f72..e8af70ce 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageCompactionJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageCompactionJsonConverter.cs @@ -42,7 +42,12 @@ public override SessionMessageCompaction Read(ref Utf8JsonReader reader, Type ty 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 SessionMessageCompaction."); diff --git a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageInfoJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageInfoJsonConverter.cs index d8b56241..c2b4a2dc 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageInfoJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageInfoJsonConverter.cs @@ -49,7 +49,12 @@ public override SessionMessageInfo Read(ref Utf8JsonReader reader, Type typeToCo 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 SessionMessageInfo."); diff --git a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageToolStateJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageToolStateJsonConverter.cs index 5ecfdd1a..cba657d1 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/SessionMessageToolStateJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/SessionMessageToolStateJsonConverter.cs @@ -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/ToolContentJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/ToolContentJsonConverter.cs index ca840236..8edbe1d7 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/ToolContentJsonConverter.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/ToolContentJsonConverter.cs @@ -41,7 +41,12 @@ public override ToolContent Read(ref Utf8JsonReader reader, Type typeToConvert, 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 ToolContent."); diff --git a/src/OpenCode.Sdk/OpenCodeRoutes.cs b/src/OpenCode.Sdk/OpenCodeRoutes.cs index 10c0e9c6..10863cba 100644 --- a/src/OpenCode.Sdk/OpenCodeRoutes.cs +++ b/src/OpenCode.Sdk/OpenCodeRoutes.cs @@ -34,8 +34,18 @@ public static class Sessions /// The escaped route. public static string GetMessage(string sessionId, string messageId) { - ArgumentException.ThrowIfNullOrEmpty(sessionId); - ArgumentException.ThrowIfNullOrEmpty(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/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 00000000..1ead1f0e --- /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/OpenCodeClientContractTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs index bfe16375..83b1d9c1 100644 --- a/tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs +++ b/tests/OpenCode.Sdk.Tests/OpenCodeClientContractTests.cs @@ -198,6 +198,69 @@ public async Task GetHealthAsync_Should_Treat_A_Malformed_Success_As_A_Protocol_ .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() { diff --git a/tests/OpenCode.Sdk.Tests/OpenCodeRoutesTests.cs b/tests/OpenCode.Sdk.Tests/OpenCodeRoutesTests.cs new file mode 100644 index 00000000..718bf5f2 --- /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/Serialization/OpenCodeJsonContextTests.cs b/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs index bd0bc925..b16352a8 100644 --- a/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs +++ b/tests/OpenCode.Sdk.Tests/Serialization/OpenCodeJsonContextTests.cs @@ -155,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() { 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 index d13f01d7..59583d65 100644 --- 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 @@ -33,6 +33,7 @@ internal sealed class PingResponseAdapter : ResponseAdapter 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)}; } @@ -74,6 +75,7 @@ internal sealed class WidgetItemResponseAdapter : ResponseAdapter= 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)}; @@ -114,6 +116,7 @@ internal sealed class WidgetOverviewResponseAdapter : ResponseAdapter= 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 index 31b57e53..ec1dd9f4 100644 --- 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 @@ -37,8 +37,18 @@ public static class OpenCodeRoutes /// The escaped route. public static string GetItem(string widgetId, string itemId) { - ArgumentException.ThrowIfNullOrEmpty(widgetId); - ArgumentException.ThrowIfNullOrEmpty(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); } 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 01929d86..41e32010 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 @@ -88,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/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs index f1a3c81f..ca2729ac 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/ResponseAdapterEmitter.cs @@ -91,6 +91,15 @@ private static MethodDeclarationSyntax EmitAdapt(OperationPlan operation) 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)), diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/RoutesEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/RoutesEmitter.cs index af64aaa3..7184756a 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/RoutesEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/RoutesEmitter.cs @@ -76,7 +76,7 @@ private static MethodDeclarationSyntax EmitBuilder(OperationPlan operation) var statements = new List(); foreach (var parameter in operation.Parameters) { - statements.AddRange(EmissionSyntax.ArgumentNullOrEmptyGuard(parameter.Name)); + statements.AddRange(EmitRouteValueGuard(parameter.Name)); } statements.Add(SyntaxFactory.ReturnStatement(EmitConcatenation(operation))); @@ -98,6 +98,35 @@ [.. operation.Parameters.Select(static parameter => .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) { diff --git a/tools/OpenCode.Sdk.Tools/Generator/Emission/UnionEmitter.cs b/tools/OpenCode.Sdk.Tools/Generator/Emission/UnionEmitter.cs index 4dcfea3e..4202d690 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Emission/UnionEmitter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Emission/UnionEmitter.cs @@ -321,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)