diff --git a/.opencode/skills/effect/SKILL.md b/.opencode/skills/effect/SKILL.md index 4929e76db9..3a44fa88dc 100644 --- a/.opencode/skills/effect/SKILL.md +++ b/.opencode/skills/effect/SKILL.md @@ -24,7 +24,6 @@ Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 - Prefer Effect `Schema` for API and domain data shapes. Use branded schemas for IDs and `Schema.TaggedErrorClass` for typed domain errors when modeling new error surfaces. - Keep HTTP handlers thin: decode input, read request context, call services, and map transport errors. Put business rules in services. - In Effect service code, prefer Effect-aware platform abstractions and dependencies over ad hoc promises where the surrounding code already does so. -- Service public methods should not leak implementation dependencies. Yield required services once while constructing the layer, close over them in the returned service implementation, and keep method return types focused on the service API rather than requiring callers to provide transitive dependencies. - Keep layer composition explicit. Avoid broad hidden provisioning that makes missing dependencies hard to see. - In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior. - Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types. diff --git a/packages/llm/AGENTS.md b/packages/llm/AGENTS.md index 77bac7bc34..9123493a43 100644 --- a/packages/llm/AGENTS.md +++ b/packages/llm/AGENTS.md @@ -28,12 +28,12 @@ const request = LLM.request({ prompt: "Say hello.", }) -const response = yield* client({ adapters: [OpenAIChat.adapter] }).generate(request) +const response = yield* LLMClient.make({ adapters: [OpenAIChat.adapter] }).generate(request) ``` -`LLM.request(...)` builds an `LLMRequest`. `client(...)` selects an adapter by `request.model.protocol`, applies patches, prepares a typed provider target, asks the adapter for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`. +`LLM.request(...)` builds an `LLMRequest`. `LLMClient.make(...)` selects an adapter by `request.model.protocol`, applies patches, prepares a typed provider target, asks the adapter for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`. -Use `client(...).stream(request)` when callers want incremental `LLMEvent`s. Use `client(...).generate(request)` when callers want those same events collected into an `LLMResponse`. +Use `LLMClient.make(...).stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.make(...).generate(request)` when callers want those same events collected into an `LLMResponse`. ### Adapters @@ -246,7 +246,6 @@ Do not blanket re-record an entire test file when adding one cassette. `RECORD=t - [x] Refactor the recorder toward extractable library boundaries: core HTTP cassette schema/matching/redaction/diffing should stay LLM-agnostic; LLM tests should supply metadata and semantic assertions from a thin wrapper. - [x] Add cassette metadata support: recorder schema version, recorded timestamp, scenario name, tags, and caller-provided subject metadata such as provider/protocol/model/capabilities without making the core recorder depend on LLM concepts. - [x] Improve replay mismatch diagnostics: show method/URL/header/body diffs and closest recorded interaction while keeping secrets redacted. Unused-interaction reporting is still TODO if a test needs it. -- [ ] Add a cassette doctor command/test helper that validates schema versions, detects secrets, checks duplicate or unused interactions where possible, and reports cassette coverage by provider/protocol/scenario. - [ ] Add semantic replay assertions for LLM cassettes: replay raw HTTP, parse provider streams, and compare normalized `LLMEvent[]` or `LLMResponse` snapshots in addition to request matching. - [ ] Add stream chunk-boundary fuzzing for text/SSE cassettes so parser tests prove correctness independent of provider chunk boundaries. - [ ] Keep deterministic coverage for malformed chunks and tool arguments that arrive in the first chunk unless a live provider reliably produces those shapes. diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 9843facaf7..89b2b182b6 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -63,12 +63,10 @@ export type StreamInput = { retries?: number toolChoice?: "auto" | "required" | "none" nativeMessages?: ReadonlyArray - // Opcode-native `Tool.Def[]` parallel to `tools` (AI SDK shape). When + // OpenCode-native `Tool.Def[]` parallel to `tools` (AI SDK shape). When // populated alongside `tools`, the LLM-native path forwards definitions to - // the model. Dispatch + multi-round tool loops land in Phase 2 step 2b; for - // now the request can carry tools but the gate keeps real production tool - // sessions on the AI SDK path because no production caller populates this - // field yet. + // the model and can dispatch multi-round tool loops without changing the + // existing AI SDK path. nativeTools?: ReadonlyArray } @@ -454,10 +452,10 @@ const live: Layer.Layer< }) }) - // ----- Phase 1: LLM-native opt-in path ----- + // ----- LLM-native opt-in path ----- // // `runNative` returns the session-shaped Stream when (and only when) the - // request matches a narrow opt-in profile we've actively wired: + // request matches the narrow opt-in profile we've actively wired: // // - The flag `OPENCODE_EXPERIMENTAL_LLM_NATIVE` is set. // - The caller populated `input.nativeMessages` with `MessageV2.WithParts` @@ -465,7 +463,8 @@ const live: Layer.Layer< // needs the typed parts). // - The bridge can route the model to one of the protocols listed in // `NATIVE_PROTOCOLS` (today: Anthropic only). - // - The session has no tools (Phase 2 will lift this). + // - If tools are present, the caller supplied a native tool definition + // for every AI SDK tool key so the native path can dispatch them. // // Otherwise it returns `undefined` and the caller falls through to the // existing AI SDK path. The return shape is deliberately narrow — we are