Skip to content

[OpenCode V2][1/5] Extract typed upstream adapter boundary without changing V1 behavior #2

Closed

Description

@tbrandenburg

Parent epic: #1 · Planned contribution: upstream PR 1/5 · Dependency: none · Related upstream: Intelligent-Internet#550

Goal

Make the existing OpenCode V1 implementation a replaceable upstream protocol adapter behind a small, typed contract while producing exactly the same public A2A behavior. This is an independently useful maintainability PR: no V2 requests, endpoint changes, new configuration defaults, or cleanup campaign.

Background

  • src/opencode_a2a/opencode_upstream_client.py (currently ~928 lines) handles HTTP transport, V1 route/payload mapping, and interrupt-request storage concerns.
  • execution/coordinator.py assumes send_message() returns a generated assistant response, while execution/stream_runtime.py and execution/stream_events.py consume V1-shaped event dictionaries and interpret session.idle.
  • The next V2 PRs must distinguish input admitted, execution update, and terminal outcome. Keep existing V1 behavior by translating V1 protocol results to these common concepts.
  • Existing tests/upstream/test_opencode_upstream_client_params.py, tests/execution/test_streaming_output_contract_*.py, tests/execution/test_session_ownership.py and scripts/live_opencode_smoke.sh are the V1 regression baseline.

KISS (keep it simple)

  • Introduce one Protocol/ABC plus a few small dataclasses/enums: e.g. SessionRef, PromptReceipt, ExecutionUpdate, ExecutionOutcome and Intervention. Use existing names/types if they fit.
  • Avoid a universal agent framework, a separate A2A gateway, a generic state machine for every OpenCode extension, or duplicate HTTP clients.
  • Preserve OpencodeUpstreamClient as a compatibility facade if necessary so callers and tests do not require mass rewrites.
  • Keep the V1 backend selected implicitly during PR 1; no public OPENCODE_API_VERSION switch yet.
  • A V1 synchronous response can produce an acceptance/result transition in the adapter: do not make admission and completion the same type.

Detailed implementation plan (junior-friendly)

  1. Baseline first. Read AGENTS.md, CONTRIBUTING.md, docs/maintainer-architecture.md and scripts/README.md. Run bash ./scripts/doctor.sh. Record the tested OpenCode 1.18.x CLI version and snapshot existing HTTP, streaming, ownership, cancellation and JSON-RPC contract tests.
  2. Map call sites. Search for OpencodeUpstreamClient/send_message/stream_events/session_prompt_async/permission_reply plus imports of execution/stream_events.py. Make a small list of what is protocol-specific vs shared. Do not move unrelated auth, persistence, A2A formatting or service modules.
  3. Define minimal internal types under a narrowly named upstream package (e.g. upstream/contracts.py). Session identifiers stay strings internally if sufficient. Admission receipt includes stable input/correlation identity when available; completion includes terminal state, output, usage/error; updates include session/message/part correlation and a normalized block type. Do not invent missing V1 identifiers: mark optional and rely on existing V1 sequencing behavior.
  4. Isolate transport from mapping. Reuse existing httpx.AsyncClient creation, Basic-auth headers, timeouts, trace headers, error redaction and request/stream concurrency budgets. Move only versioned HTTP paths, request payloads and response parsing into a V1 backend. Ensure 204 and non-JSON failures remain handled appropriately without changing today's V1 contract.
  5. Normalize V1 events at the boundary. Extract just enough from stream_events.py for the shared runtime to consume typed updates (text/reasoning/tool, usage, errors, idle, permissions/questions). Keep streaming deduplication, ordering, final snapshot and A2A artifact emission shared and unchanged. Avoid raw V1 event dictionaries escaping into code that V2 will also use.
  6. Preserve extension behavior. Keep the current session, provider, workspace and interrupt JSON-RPC handler call surface working through the V1 adapter/facade. Do not create a new generalized extension interface for every endpoint.
  7. Wire the new boundary incrementally. Start with session creation and send + stream, then move only the necessary event parsing. Keep the diff limited; if a planned type causes widespread churn, simplify it.
  8. Prove future fit separately. In a scratch branch or throwaway script (not PR 1), sketch how a V2 inbox receipt and subsequent completion would use the types. Record the result in the PR rationale; do not merge unfinished V2 code with the preparatory refactor.
  9. Add focused adapter mapping/unit tests and run the existing suites; compare V1 HTTP request shapes and A2A output fixtures before and after. Update the internal architecture documentation only where changed.

Pitfalls to avoid

  • Treating session.idle as a universal (V1+V2) correlated terminal signal.
  • Defining submit_prompt() -> final assistant message in the new interface.
  • Moving authorization/session ownership/SQLite persistence into the adapter.
  • Broad renames across every JSON-RPC handler, output renderer and A2A public schema.
  • Making V1 smoke assertions weaker to get the refactor to pass.

Final manual E2E acceptance test (mandatory before merging PR 1)

Prerequisites: a real OpenCode V1 1.18.x binary, a real configured model for prompts, isolated test workspace, upstream Basic-auth password, inbound A2A auth, local ports and throwaway HOME/XDG config.

  1. Run bash ./scripts/live_opencode_smoke.sh without LIVE_SMOKE_SKIP_PROMPT=1. Verify upstream auth is enforced and a streamed prompt returns expected text and TASK_STATE_COMPLETED.
  2. Use the configured A2A endpoint to send a non-streaming message; verify terminal task, assistant artifact and no partial-only result.
  3. Send a second message with the same session-binding context and verify session continuity. Check one supported JSON-RPC read-only session/provider method still works.
  4. Start a long task, cancel it, and verify the expected cancelled state. Make an unauthorized request and a cross-owner session request; verify rejection.
  5. Inspect emitted text/reasoning/tool metadata (where the selected model produces it), ensure final artifacts are not duplicated, and check logs contain no credentials or full prompt bodies by default.
  6. Record actual CLI/server versions, exact commands and pass/fail evidence in the upstream PR. If a live model cannot run, do not mark manual E2E passed.

Acceptance criteria

  • Small typed adapter contract has distinct admission, updates and completion concepts.
  • V1 is the only active backend; config defaults and A2A/JSON-RPC/Agent Card contracts are unchanged.
  • Raw V1 event shapes are confined to the V1 decoding path.
  • V1 request/response semantics, permissions, auth, workspace, and stream output remain behaviorally identical.
  • Existing unit, HTTP-boundary, output/ownership tests and bash ./scripts/doctor.sh pass.
  • Real V1 send/stream/follow-up/cancel smoke passes with evidence.
  • Unrelated refactoring and speculative V2 abstractions are absent.

Definition of Done

Activity

  1. tbrandenburg commented on Oct 25, 2026

    @tbrandenburg
    OwnerAuthor

    Preparation PR 更新

    Typed boundary 的两个 HIGH regressions(非 completed outcome 被当 success;invalid submit result 导致 legacy resend)已以 append-only commit 修复并追加到上游 PR:Intelligent-Internet#551 。Exact head 65c15e9 的 doctor:841 passed,92.87% coverage;4 个 safety regressions 通过。

    后续 restricted profile 不重写该基础。完整 scope / live 证据见本仓库 #1 的最新状态评论与 #7;原 issue 保持 open,等待 upstream review/merge 和正式 disposition。未 force-push 或自动 merge。

  2. tbrandenburg commented on Oct 25, 2026

    @tbrandenburg
    OwnerAuthor

    Fork scope 完成:明确 merge/reconciliation

    #7 已 normal merge 到本 fork main 19943955704bf05347619ca5b98addb94a683c97;experimental fork prerelease 已发布,未发布 PyPI。

    Typed admission/update/outcome boundary、兼容 V1 facade 与 outcome-safety fixes 已交付。Candidate 与 main 分别重新运行 doctor:1021 passed / 0 failed,conformance 分别 8/8;main 36 个 default/opt-in tests 通过。Recorded real V1 1.18.19 regression source digest 与 released source 一致;本轮没有冒称重跑完整 model matrix。Pinned evidence。

    按本 issue “merge/reconciliation 后关闭”的约定,将本次获授权的 fork merge 明确记录为 fork delivery reconciliation,关闭本 typed-boundary implementation issue。这不代表 upstream merge:Intelligent-Internet#551 仍 open,贡献分支保持原样供审查。其 follow-up safety patch 在 Intelligent-Internet#552 中有 patch-equivalent commit;不为 graph 美观改写历史。完整 upstream/广义范围由 #1 继续跟踪。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions