Adds opt-in `nativeTools?: ReadonlyArray<Tool.Def>` to `LLM.StreamInput`
so callers that route through the native path can attach typed
opencode tool definitions alongside the AI SDK `tools` record. The
gate in `runNative` widens accordingly: a session can use the native
path when it has zero tools (existing behavior) OR when it explicitly
provides `nativeTools` matching its AI SDK `tools` (new opt-in). When
`nativeTools` reaches `LLMNative.request`, the existing
`toolDefinition` converter folds each `Tool.Def` into the request's
`tools` array and the LLM core lowers it onto the wire.
This commit deliberately does NOT include the dispatch loop. A
session that opts in by setting `nativeTools` and that triggers a
`tool-call` from the model will see the call event but no
`tool-result` because the native path has no execute handler yet.
That's why no production caller populates `nativeTools`: phase 2
step 2b will land the dispatch loop and only then will real
production sessions route through here.
What this lays in place:
- `StreamInput.nativeTools` typed against `Tool.Def[]` from `@/tool`.
Aliased to `OpenCodeTool` at the import to dodge a clash with the
AI SDK `Tool` type that the same file already imports.
- The `runNative` gate flips from "no tools allowed" to "either no
tools, or `nativeTools` is supplied". An AI SDK tool count > 0
with `nativeTools` undefined still falls through, so existing
production sessions are unaffected.
- `LLMNative.request` already accepted `tools: ReadonlyArray<Tool.Def>`
and converts via `toolDefinition`. We just forward the input
through; no LLM-bridge change.
Smoke coverage: a new test in `llm-native-stream.test.ts` builds a
typed `Tool.Def` (Effect Schema parameters), routes it through
`LLMNative.request` + `LLMClient.prepare`, and asserts the prepared
Anthropic target carries the tool as an `input_schema` block with
the expected JSON Schema shape. This validates the conversion path
that phase 2 step 2b will exercise from inside `runNative`.
Verification: opencode typecheck clean; 35/0/0 across the three
bridge-area tests (`llm-native.test.ts`, `llm-native-stream.test.ts`,
`llm-bridge.test.ts`).
Adds `test/session/llm-native-stream.test.ts` — one focused test that
proves the end-to-end wire-up `runNative` relies on actually produces
session events from a scripted Anthropic SSE response.
The test stays self-contained:
- Builds a fake Anthropic `Provider.Info` + `Provider.Model` via
`ProviderTest`.
- Builds an `LLMRequest` via `LLMNative.request(...)` from a
`MessageV2.WithParts` user message — the same call shape `runNative`
uses inside `session/llm.ts`.
- Creates an `LLMClient` with the same adapters list + `ProviderPatch.defaults`
list as `runNative`. The adapters are imported directly from
`@opencode-ai/llm`; if `runNative`'s `NATIVE_ADAPTERS` array changes,
this test's `adapters` constant has to follow (commented).
- Provides a single fixed-response HTTP layer that returns a scripted
Anthropic SSE body. The layer helper is inlined (12 lines) rather
than imported from `packages/llm/test/lib/http.ts` so the test
doesn't reach across package boundaries.
- Pipes the LLM stream through `LLMNativeEvents.mapper()` exactly as
`runNative` does (`Stream.flatMap` + lazy `Stream.concat` for
flush), runs it to completion, and asserts the key session events:
`text-start` precedes `text-delta`, `finish-step` carries
`finishReason: "stop"`, and `finish` carries the merged usage totals.
This does NOT test the dispatch gate inside `session/llm.ts`
(`!Flag.OPENCODE_EXPERIMENTAL_LLM_NATIVE`, missing `nativeMessages`,
tools present, non-Anthropic protocol). Those are simple boolean
conditions and don't need separate coverage. It also does not exercise
the production `Service` layer — that's deferred to Phase 2 step 2
(tool support) and Phase 2 step 3 (production caller wiring).
What the test buys: confidence that the conversion pipeline works and
catches regressions in `LLMNative.request`, the LLM adapter set, or
`LLMNativeEvents.mapper` before they would surface in a real session.
Verification: 34/0/0 across the three bridge-area tests
(`llm-native.test.ts` + `llm-native-stream.test.ts` +
`llm-bridge.test.ts`); opencode typecheck clean.