Skip to content

feat(opencode): native Anthropic advisor tool - #48438

Open
alohaninja wants to merge 5 commits into
anomalyco:devfrom
alohaninja:native-advisor
Open

feat(opencode): native Anthropic advisor tool#48438
alohaninja wants to merge 5 commits into
anomalyco:devfrom
alohaninja:native-advisor

Conversation

@alohaninja

@alohaninja alohaninja commented Sep 11, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #23058

Draft on purpose. #23058 is assigned to @jlongster, and #23436 (a subagent-style advisor) was closed with a preference for plugins. This can't be a plugin: the consultation happens inside the executor's own response stream (server_tool_useadvisor_tool_result with pause_turn), so it needs stream handling, history replay, and cost accounting in core. Happy to close this if a maintainer already has an approach, or to split it if the shape is right but too big.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds per-agent opt-in for Anthropic's server-side advisor tool (advisor_20260301):

{ "agent": { "build": { "advisor": { "model": "claude-opus-4-6", "maxUses": 1 } } } }

The executor (e.g. Sonnet) can pause mid-turn and ask a stronger model; Anthropic runs that consultation and returns the advice in the same response. OpenCode never sends a second request.

What had to exist for this to work correctly:

  • Replay. Native blocks must go back to Anthropic at their original position on later turns, including across a restart. The result lives on the tool part; the receiving step-finish records the response order (metadata.opencodeAdvisor.entries). Message conversion follows that ledger. Fork remaps the part references.
  • Continuation. pause_turn is resumed in-process, capped at 3 advisor-only resumptions per turn. A pending call is terminal on cancel/crash/restart: marked abandoned, replayed as a text marker, never resumed. A resumption request is not retried once its response starts, because a retry re-runs (and re-bills) the consultation.
  • Cost. Advisor tokens come from usage.iterations and are priced with the advisor model's catalog cost, added once to the step/session, and kept out of the executor's context total. Missing pricing shows a partial-cost notice rather than understating.
  • Degradation. When the model, route, or credentials can no longer carry the beta (model switch, OAuth login, proxy baseURL, advisor: false), advice is replayed as plain text so the session keeps working. Export and share redact the payload; encrypted content never leaves the DB.
  • Guardrails. Direct Anthropic API with API-key auth only. permission.advisor must be allow/deny (ask can't pause a server-side call). Incomplete settings error with the agent name before anything is sent. The V2 native runtime rejects it explicitly instead of dropping it.

Config, schema, and V2 guard are in packages/core + packages/schema; everything runtime is in packages/opencode/src/session. The chore: generate commit is only the new advisor field in the SDK/client types. Docs: new "Advisor" section in agents.mdx (English only; locale sync is automated).

I don't have a live capture of pause_turn usage accounting — the fixture assumes Anthropic reports the advisor iteration once per response, not cumulatively across a pause/resume. If that's wrong, cost would double-count on paused turns. Flagging rather than guessing.

How did you verify your code works?

  • bun turbo typecheck --force (30/30), check:generated, test:httpapi (208/208), core:test (1114/1114), opencode:test, prettier on all changed files.
  • New tests: schema/config layering and migration; SDK wire contract against a fake transport (exact block types, call IDs, betas, no advertised tool in history-only mode); processor ledger/pending/cost (incl. duplicate step-finish idempotence and the no-retry cases); prompt loop pause bound, cancel, restart abandonment, incomplete config; message conversion for same-response, cross-response, two-call, no-receipt, errored, fork-shaped, and cross-model histories; export/share redaction; fork remap; an HTTP-API test that boots the real server with a scripted Anthropic transport, consults, restarts, and replays with advisor: false.
  • Manual: test/fixture/advisor-server.ts boots the real server with scripted SSE (pause, complete, encrypted, error, cancel) and logs each request's advertised/pending/completed + beta header. Ran every scenario, a restart mid-session, and the advisor: false follow-up through a client on the server API. Procedure in test/fixtures/advisor-smoke/README.md.
  • Live against the real Anthropic API with Sonnet 4.6 + Opus 4.6: real srvtoolu_… call, plaintext result, advisor cost 0.087 inside a 0.102 step (matches Opus list price on the reported iteration tokens), executor context excluded the advisor tokens, restart + follow-up replayed cleanly, advisor: false follow-up produced no tool.

Screenshots / recordings

No UI changes in this repo; the advisor renders through the existing tool-part path.

pr48438-native-advisor

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Add `agent.<name>.advisor` (`{ model, maxUses } | false`) to the shared
schema, the V1/V2 agent configs, markdown agent frontmatter, and the V1
migration. Partial objects merge across config layers and are validated
once at request time. The native V2 session runner rejects an enabled
advisor with UnsupportedApiError instead of silently dropping it.
Register Anthropic's server-side advisor tool (`advisor_20260301`) for
agents that opt in, on the direct Messages API with API-key auth only.
Consultations arrive inside the executor's own response stream; OpenCode
never issues a second request for the advice.

- Persist the result on the originating tool part and a response-order
  ledger on the receiving step-finish so later turns replay the native
  blocks at their original position. Ledger references survive
  `Session.fork`; unresolvable references fall back to text.
- Resume `pause_turn` automatically, at most three advisor-only
  resumptions per turn. Pending calls are terminal on cancel, crash, or
  restart: they are marked abandoned and never resumed, and a resumption
  request is never retried once its response has started.
- Price advisor usage from `usage.iterations` with the advisor model's
  catalog cost, add it once to step/session cost, and keep it out of the
  executor's context tokens. Missing pricing surfaces a partial-cost notice.
- Replay advice as plain text when the model, route, or credentials can no
  longer carry the beta, and for export/share (encrypted content is never
  emitted). Compaction keeps a consultation's call and result together.
- Reject `ask` permission, tool-name collisions, unsupported routes, and
  incomplete settings with explicit errors before anything is sent.

Includes a scripted-transport fixture (`test/fixture/advisor-*.ts`) that
boots the real server and drives pause, encrypted, error, and cancel
scenarios without paid inference, plus a manual smoke harness.
Render only identifier-shaped advisor error codes into replayed history,
decode each advisor part once during history conversion, reset the
per-step result flag on step start, and name the tier-selection token
sum for what it is.
@alohaninja
alohaninja marked this pull request as ready for review September 11, 2026 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Anthropic "advisor strategy"

1 participant