Repository navigation
feat(sdk): land the M1 callable client - #16
Merged
Merged
Conversation
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
- 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
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)
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.
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.
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.
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.
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.
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+.
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.
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.
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.
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.
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.
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.
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.
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.
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.
…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.
This was referenced Aug 13, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M1 — walking skeleton, complete
This branch closes M1: the SDK goes from a compiled model closure (#15) to a callable client demonstrated live against a real server. It also executes the v2 retarget sealed in ADR-0005 — the generation target moves to upstream's
packages/protocol/openapi.jsonon thev2branch, pinned at commita6a712a, and the legacy 1.x surface is never built.What landed
a6a712a(104 paths / 120 operations / 324 schemas), the single-element validation-onlyallOfadmit rule in the ingestion wall, and the regenerated model closure.OpenCodeClient.GetHealthAsync,Sessions.GetSessionClient(...),SessionClient.GetMessageAsync, guarded envelopes,OpenCodeRoutes(nested per-group containers, template + escaped-builder pairs), and response adapters as committed source. Emitters stay name-blind; names ride the plan.Pipelineowns endpoint authority, Basic-auth/User-Agent decoration, response buffering, throw-versus-NoThrow, and transport-failure mapping over the ADR-0007 exception spine (OpenCodeException→OpenCodeApiException; transport/protocol failures asOpenCodeTransportException).envelopePayloadNamesis an override ledger for collisions only); the User-Agent version token strips+build metadata and omits itself entirely rather than falling back silently.PublicApiGeneratorbaseline shared across TFMs; the generation writer refuses unmanifested overwrites and headerless manifest entries.tests/OpenCode.Sdk.Sandbox, a committed debug playground for driving the SDK against a realopencode2 serveunder a debugger.Packing still fails deliberately on the
.generation-incompletemarker — the generation profile is two operations wide; packaging unblocks when the surface completes (M5).Live demonstration (2026-08-13)
Server: opencode2 v0.0.0-next-17403 (
@opencode-ai/cli@next); spec pin: v2-brancha6a712a.Content detail (second run):
Version skew note
The demo server is
opencode2v0.0.0-next-17403 while the spec pin is v2-branch commita6a712a— deliberately not the same snapshot. Pinned-snapshot policy (ADR-0005) is the mechanism that absorbs upstream's continuous pre-GA churn: refreshes are deliberate, at milestone boundaries (M2 is the next sanctioned moment), never HEAD-tracking.Review follow-ups
Multi-agent review findings triaged into issues #17–#25 (milestone-anchored; see
docs/ROADMAP.md). The blocker set and the performance-test infrastructure landed on this branch.Fixes #17. Fixes #18.