0e558e13c7
Wires the prompt-side tool resolver to also surface opencode-native
`Tool.Def[]` alongside the AI SDK record it already builds. With
`OPENCODE_EXPERIMENTAL_LLM_NATIVE=1` set, real production sessions
that satisfy the gate now stream through `LLMNativeTools.runWithTools`
instead of `streamText` — the LLM-native path goes from
"plumbing-only" to "actually used."
Changes:
- `prompt.ts:resolveTools` collects `Tool.Def[]` from the registry
loop and tracks a feasibility flag. MCP tools (which only have AI
SDK shape) flip the flag off; the synthesized `StructuredOutput`
tool that the json_schema branch injects also flips it. The return
shape becomes `{ tools, nativeTools }` where `nativeTools` is
`undefined` whenever any non-registry tool source contributes —
callers fall through to the AI SDK path automatically. The
registry path stays in sync because every `tools[item.id] =
tool({...})` is paired with a `nativeTools.push(item)` at the same
loop iteration.
- The single caller (`prompt.ts:1396`) destructures the new shape
and passes `nativeTools` through to `handle.process(...)`. The
json_schema branch sets `nativeTools = undefined` after injecting
`StructuredOutput` so the gate falls through for structured-output
sessions.
- `runNative` (in `session/llm.ts`) gains two safety nets that work
regardless of caller behavior:
1. Coverage check: if AI SDK tools are non-empty, every key must
have a matching `Tool.Def` in `nativeTools`. A partial set
falls through. Defends against future callers that might
emit a partial native list.
2. Filter parity: `runNative` now calls the existing
`resolveTools(input)` (the in-file permission/user-disabled
filter) and intersects its keys with `nativeTools`, then
feeds the filtered AI SDK record to the dispatcher and the
filtered native list to `LLMNative.request`. Without this,
sessions could see permission-disabled tools advertised on
one path but not the other.
- The dispatch path uses the filtered AI SDK tools record as the
execute table: `LLMNativeTools.runWithTools({ tools:
filteredAITools, ... })`. Tool definitions sent to the model are
the filtered native list. Every tool the model sees can dispatch.
What this enables: a session opted into the experimental flag, with
a clean toolset (registry-only, no MCP, no structured output),
running an Anthropic model, now exercises the streaming-dispatch
loop end-to-end. Tool calls fire as soon as the model finishes
streaming each tool's input; results land in the stream the moment
each handler resolves. Multi-round behavior matches phase 2 step 2b.
What this still does NOT do (deferred to step 4):
- Parity test harness comparing native vs AI SDK event sequences for
the same scripted session. Until that lands, broader confidence
comes from running real sessions with the flag set.
- MCP support on the native path. Sessions with MCP servers
configured stay on AI SDK indefinitely.
- Native support for the synthesized `StructuredOutput` tool.
Verification: opencode typecheck clean for `src/session/*` (the
TUI-side errors visible in the working tree are Kit's parallel
work, untouched here); bridge area tests 36/0/0 across
`llm-native.test.ts` + `llm-native-stream.test.ts` +
`llm-bridge.test.ts`; `prompt.test.ts` still 47/0/0 (no regression
from the resolveTools shape change).