diff --git a/packages/agent-core-v2/docs/en/llm.md b/packages/agent-core-v2/docs/en/llm.md index 41f9aad47a6..4219cd40e29 100644 --- a/packages/agent-core-v2/docs/en/llm.md +++ b/packages/agent-core-v2/docs/en/llm.md @@ -6,9 +6,9 @@ llm is a standalone LLM request library inside the human layer (`src/human/llm/` 1. **Minimal boundary: llm = "a single request"**. llm only handles request encoding/decoding and event emission. auth, usage accounting, HistoryMessage/meta, compaction, switch, the media file system, and Tool Message assembly are all out of scope — they either move up to the turn/agent layer or plug in as contribution points. 2. **Streaming-native; events are the contract**. The only outward surface is a single, purely serializable event stream (requester level: `llm.sent / streaming.headers / streaming.part / streaming.usage / streaming.finish / streaming.message_id / failed.syntax / failed.remote / done`; the turn level adds `llm.retrying / llm.recovering`, and `llm.sent` carries the most recent recovery record). Streaming and non-streaming are isomorphic (non-streaming also accumulates over the stream, just without deltas). Events are emitted as they arrive — no caching, no fallback. -3. **format masks inter-protocol differences; traits express provider customizations**. format lives at the protocol layer and handles encoding/decoding of requests, responses, errors, usage, and finish. Each protocol owns a typed trait interface (`OpenAITrait` / `OpenAIResponsesTrait` / `AnthropicTrait` / `GoogleGenAITrait`) exposing only the customization points that protocol actually consumes — a hook a protocol ignores is unrepresentable, never silently dead. format and trait never import each other: both speak only the neutral wire/chunk types in the protocol's `contract.ts`. The requester is the composition root — `generate` runs a fixed per-protocol pipeline (`planOpenAIRequest` and friends) that alternates pure format stages (lower → assemble → encode → stream parser) with trait hooks (cacheKey/thinking → convertMessage → mergeHistory → convertTool → buildParams → extractUsage), so customization is explicit data flow instead of a closure captured inside format. Endpoint/env resolution and default headers form the provider `connection`, error classification is a requester option, and model capability is a provider-variant field — none of them are format business. Each base's public seam is contract + trait + requester; format, lower, and patterns are internal to the requester pipeline — only bases code and tests may import them (lint-enforced). Protocol differences must not leak into the turn or into requester decorators. +3. **format masks inter-protocol differences; traits express provider customizations**. format lives at the protocol layer and handles encoding/decoding of requests, responses, errors, usage, and finish. Each protocol owns a typed trait interface (`OpenAITrait` / `OpenAIResponsesTrait` / `AnthropicTrait` / `GoogleGenAITrait`) exposing only the customization points that protocol actually consumes — a hook a protocol ignores is unrepresentable, never silently dead. format and trait never import each other: both speak only the neutral wire/chunk types in the protocol's `contract.ts`. The requester is the composition root — `generate` runs a fixed per-protocol pipeline (`planOpenAIRequest` and friends) that alternates pure format stages (lower → assemble → encode → stream parser) with trait hooks (cacheKey/thinking → convertMessage → mergeHistory → convertTool → buildParams → extractUsage), so customization is explicit data flow instead of a closure captured inside format. Endpoint/env resolution and default headers form the provider `connection`, error classification is a requester option, and model capability is a provider-binding field — none of them are format business. Each base's public seam is contract + trait + requester; format, lower, and patterns are internal to the requester pipeline — only bases code and tests may import them (lint-enforced). Protocol differences must not leak into the turn or into requester decorators. 4. **Two-layer error model**. Internally, code throws the SDK's native errors; local request validation throws the shared `SyntaxRequestFormatError` (`llm/syntax-errors.ts`), which the requester converts uniformly via `toLlmSyntaxErrorMessage`, with no intermediate layer. Externally there are only `llm.failed.syntax` (local message syntax errors, never retried) and `llm.failed.remote` (remote streaming errors, subdivided into connection / timeout / rate_limit / quota_exhausted / context_overflow / request_structure, etc.), converted by format at the boundary. -5. **Stateless core + turn-driven orchestration**. `generate(config, content, control)` is a stateless function; errors are delivered via onEvent, never thrown. The turn machine invokes the request actor (`createRequestActor`) directly: the actor wraps a single request (messageResolvers, abort scope, event sendBack), and the turn drives retry and recovery through the pure policy functions in retry.ts / recovery.ts: recovery is a strategy chain composed by the caller (the engine tries `credentialsRecovery` before the configured replacement-message strategies such as media degradation); each strategy's pure `propose` returns a self-describing record (`strategy`/`action`, optional replacement `messages`, optional opaque `prepare` effect) — the turn runs `prepare` and/or swaps messages and re-enters `thinking` with attempt reset to 1; retry backs off in the `retrying` state (honoring Retry-After), and the turn emits `llm.recovering / llm.retrying` for each. Empty response is judged by the turn at `llm.done` via the pure `emptyResponseError` and re-raised as `llm.failed.remote`, entering the same failure cascade. Abort is carried by an AbortController owned by the turn: the controller is passed into the request actor via `LlmInput.signal`, and the turn aborts it directly on `turn.abort`, with the request ending as `llm.failed.remote`; the request actor neither creates its own controller nor touches any signal on teardown, so a finished request can never abort a shared signal. The accumulator is held by the turn and fed by the event stream; on `llm.retrying / llm.recovering` the turn rolls it back and recreates it, so every attempt accumulates from zero while as much interrupted state as possible is preserved (the turn finishes the complete message out of the accumulator at `llm.done`). +5. **Stateless core + turn-driven orchestration**. `generate(config, content, control)` is a stateless function; errors are delivered via onEvent, never thrown. The turn machine invokes the request actor (`createRequestActor`) directly: the actor wraps a single request (messageResolvers, abort scope, event sendBack), and the turn drives retry and recovery through the pure policy functions in retry.ts / recovery.ts: recovery is a strategy chain composed by the caller (the engine tries `credentialsRecovery` before the configured replacement-message strategies such as media degradation); each strategy's pure `propose` returns a self-describing record (`strategy`/`action`, optional replacement `messages`, optional opaque `beforeRetry` effect) — the turn runs `beforeRetry` and/or swaps messages and re-enters `thinking` with attempt reset to 1; retry backs off in the `retrying` state (honoring Retry-After), and the turn emits `llm.recovering / llm.retrying` for each. Empty response is judged by the turn at `llm.done` via the pure `emptyResponseError` and re-raised as `llm.failed.remote`, entering the same failure cascade. Abort is carried by an AbortController owned by the turn: the controller is passed into the request actor via `LlmInput.signal`, and the turn aborts it directly on `turn.abort`, with the request ending as `llm.failed.remote`; the request actor neither creates its own controller nor touches any signal on teardown, so a finished request can never abort a shared signal. The accumulator is held by the turn and fed by the event stream; on `llm.retrying / llm.recovering` the turn rolls it back and recreates it, so every attempt accumulates from zero while as much interrupted state as possible is preserved (the turn finishes the complete message out of the accumulator at `llm.done`). 6. **No silent fallback**. Configuration is taken exactly as given. For beta features, thinking, empty response, and similar scenarios, define explicit error conditions first, fail at request time, and guide the user to fix the configuration — never fall back silently. 7. **Every variable capability is a contribution point**. Providers, media upload/degradation, usage, traceId, and error recovery (compaction / media degradation) all plug in through extension points; the llm core contains none of these concepts. 8. **Data is data**. A model is pure, function-free data (endpoint url + model uniquely identifies a model), serializable and directly usable as generate input. The catalog is a derived `provider -> models` cache; the dependency direction only goes from models-dev into llm internals, never the reverse. @@ -37,7 +37,7 @@ llm/ │ │ LlmRequestConfig.credentials: credential contribution point │ │ (resolve/canRecover/invalidate), resolved per attempt by the caller; │ │ factories and the credentialsRecovery strategy live in human/credentials -│ │ (staticCredentials / oauthCredentials; kimiOAuthCredentialProvider adapts +│ │ (createStaticCredentialProvider / createOAuthCredentialProvider; kimiOAuthCredentialProvider adapts │ │ Kimi OAuth tokens); the runWithCredentialRecovery / │ │ streamWithCredentialRecovery executors for direct callers live in │ │ llm-adapter/model/credential-recovery @@ -60,7 +60,7 @@ llm/ └── media/ media contribution points: cache / degrade / ref / resolver / store / upload ``` -Request lifecycle: `generate` receives (config, content, control) → the caller resolves `config.credentials` into a fully-credentialed model before each attempt (the request actor on the machine path), so requests always carry fresh credentials and a credential-refresh recovery (recoverable 401 → `credentials.invalidate()`, emitted as `llm.recovering` with strategy `credentials`) naturally re-resolves on the re-send (direct callers outside the state machines — ping, generate, full compaction, media upload — share the same single-retry recovery through `runWithCredentialRecovery` / `streamWithCredentialRecovery`) → the requester's `plan*` function composes pure format stages with trait hooks into protocol requestParams (format lowers the generic Message[] through the Pattern Rewriter; trait adjusts kwargs, converted messages, history, tools, and final params in between) → internalGenerate calls the official SDK → streaming chunks are converted by the stateless parser callbacks into `llm.streaming.part / streaming.usage / streaming.finish / streaming.message_id` events → errors are converted by format into `llm.failed.*`; on success the requester emits `llm.done`, on failure it ends with `llm.failed.syntax / llm.failed.remote` and never emits `llm.done`. At `llm.done` the turn judges empty responses via `emptyResponseError` and re-raises them as `llm.failed.remote`; the turn machine first tries recovery on `llm.failed.remote` (the engine-composed strategy chain — credential refresh on a recoverable 401 first, then replacement-message strategies — each pure `propose` returning a record whose opaque `prepare` effect the turn executes, emitting `llm.recovering`), then retries with backoff (honoring Retry-After, emitting `llm.retrying`), and only fails the turn once attempts are exhausted. The turn holds the HistoryAccumulator, fed by the event stream, rolls it back and recreates it on `llm.retrying / llm.recovering`, and finishes the complete message at `llm.done`; usage accounting, tracing, compaction, and media degradation all attach to the event stream as plugins/contribution points. +Request lifecycle: `generate` receives (config, content, control) → the caller resolves `config.credentials` into a fully-credentialed model before each attempt (the request actor on the machine path), so requests always carry fresh credentials and a credential-refresh recovery (recoverable 401 → `credentials.invalidate()`, emitted as `llm.recovering` with strategy `credentials`) naturally re-resolves on the re-send (direct callers outside the state machines — ping, generate, full compaction, media upload — share the same single-retry recovery through `runWithCredentialRecovery` / `streamWithCredentialRecovery`) → the requester's `plan*` function composes pure format stages with trait hooks into protocol requestParams (format lowers the generic Message[] through the Pattern Rewriter; trait adjusts kwargs, converted messages, history, tools, and final params in between) → internalGenerate calls the official SDK → streaming chunks are converted by the stateless parser callbacks into `llm.streaming.part / streaming.usage / streaming.finish / streaming.message_id` events → errors are converted by format into `llm.failed.*`; on success the requester emits `llm.done`, on failure it ends with `llm.failed.syntax / llm.failed.remote` and never emits `llm.done`. At `llm.done` the turn judges empty responses via `emptyResponseError` and re-raises them as `llm.failed.remote`; the turn machine first tries recovery on `llm.failed.remote` (the engine-composed strategy chain — credential refresh on a recoverable 401 first, then replacement-message strategies — each pure `propose` returning a record whose opaque `beforeRetry` effect the turn executes, emitting `llm.recovering`), then retries with backoff (honoring Retry-After, emitting `llm.retrying`), and only fails the turn once attempts are exhausted. The turn holds the HistoryAccumulator, fed by the event stream, rolls it back and recreates it on `llm.retrying / llm.recovering`, and finishes the complete message at `llm.done`; usage accounting, tracing, compaction, and media degradation all attach to the event stream as plugins/contribution points. ## Rejected Schemes (do not reintroduce) diff --git a/packages/agent-core-v2/docs/zh/llm.md b/packages/agent-core-v2/docs/zh/llm.md index ef5c9ff0f4b..65879ec1f51 100644 --- a/packages/agent-core-v2/docs/zh/llm.md +++ b/packages/agent-core-v2/docs/zh/llm.md @@ -6,9 +6,9 @@ llm 是 human 层内一个独立的 LLM 请求库(`src/human/llm/`),提供 1. **边界极简:llm = 「一次请求」**。llm 只负责请求编解码与事件回传。auth、usage 统计、HistoryMessage/meta、compaction、switch、媒体文件系统、Tool Message 拼装全部不属于 llm——要么上移到 turn/agent 层,要么以贡献点接入。 2. **流式原生、事件即契约**。对外只暴露一条纯可序列化的事件流(requester 层:`llm.sent / streaming.headers / streaming.part / streaming.usage / streaming.finish / streaming.message_id / failed.syntax / failed.remote / done`;turn 层补充 `llm.retrying / llm.recovering`,`llm.sent` 携带最近一次 recovery 记录),流式与非流式同构(非流式也走流式累积,只是不发 delta);事件收到即发,不缓存、不兜底。 -3. **format 屏蔽协议间差异,trait 表达 provider 定制**。format 位于 protocol 层,负责请求、响应、错误、usage 和 finish 的编解码。每种协议拥有自己的类型化 trait 接口(`OpenAITrait` / `OpenAIResponsesTrait` / `AnthropicTrait` / `GoogleGenAITrait`),只暴露该协议实际消费的定制点——协议不支持的 hook 在类型上无法表达,而不是配了却静默无效。format 与 trait 互不 import:双方只共享协议 `contract.ts` 里的中立 wire/chunk 类型。requester 是组合根——`generate` 执行每个协议固定的流水线(`planOpenAIRequest` 等),交替调用纯 format 阶段(lower → assemble → encode → stream parser)与 trait hooks(cacheKey/thinking → convertMessage → mergeHistory → convertTool → buildParams → extractUsage),定制逻辑是显式的数据流,而不是捕获在 format 闭包里。endpoint/环境变量解析与默认 headers 属于 provider `connection`,错误归类是 requester 选项,模型能力是 provider variant 字段——都不是 format 的职责。每个 base 的公开接缝是 contract + trait + requester;format、lower、patterns 是 requester 流水线的内部模块——只有 bases 内代码和测试可以 import(lint 强制)。协议差异不允许泄漏到 turn 或 requester 的装饰层。 +3. **format 屏蔽协议间差异,trait 表达 provider 定制**。format 位于 protocol 层,负责请求、响应、错误、usage 和 finish 的编解码。每种协议拥有自己的类型化 trait 接口(`OpenAITrait` / `OpenAIResponsesTrait` / `AnthropicTrait` / `GoogleGenAITrait`),只暴露该协议实际消费的定制点——协议不支持的 hook 在类型上无法表达,而不是配了却静默无效。format 与 trait 互不 import:双方只共享协议 `contract.ts` 里的中立 wire/chunk 类型。requester 是组合根——`generate` 执行每个协议固定的流水线(`planOpenAIRequest` 等),交替调用纯 format 阶段(lower → assemble → encode → stream parser)与 trait hooks(cacheKey/thinking → convertMessage → mergeHistory → convertTool → buildParams → extractUsage),定制逻辑是显式的数据流,而不是捕获在 format 闭包里。endpoint/环境变量解析与默认 headers 属于 provider `connection`,错误归类是 requester 选项,模型能力是 provider binding 字段——都不是 format 的职责。每个 base 的公开接缝是 contract + trait + requester;format、lower、patterns 是 requester 流水线的内部模块——只有 bases 内代码和测试可以 import(lint 强制)。协议差异不允许泄漏到 turn 或 requester 的装饰层。 4. **错误两层模型**。内部 throw SDK 原生错误;本地请求校验抛共享的 `SyntaxRequestFormatError`(`llm/syntax-errors.ts`),由 requester 经 `toLlmSyntaxErrorMessage` 统一转换,不加中间层。对外只有 `llm.failed.syntax`(本地消息语法错误,不重试)与 `llm.failed.remote`(远程流式错误,细分为 connection/timeout/rate_limit/quota_exhausted/context_overflow/request_structure 等),由 format 在边界完成转换。 -5. **无状态内核 + turn 驱动的编排**。`generate(config, content, control)` 是无状态函数,错误走 onEvent 不 throw;turn machine 直接 invoke 请求 actor(`createRequestActor`):actor 包装单次请求(messageResolvers、abort 作用域、事件 sendBack),turn 借助 retry.ts / recovery.ts 的纯策略函数驱动重试与 recovery:recovery 是一条由调用方组装的策略链(engine 先尝试 `credentialsRecovery`,再尝试配置的媒体降级等替换消息策略),每个策略的纯函数 `propose` 返回自描述记录(`strategy`/`action`、可选替换 `messages`、可选不透明 `prepare` 副作用),turn 执行 `prepare` 和/或替换消息并重进 `thinking`(attempt 重置为 1),重试走 `retrying` 状态的 backoff(尊重 Retry-After),两者分别由 turn 对外补发 `llm.recovering / llm.retrying` 事件;empty response 由 turn 在 `llm.done` 时经纯函数 `emptyResponseError` 判定并重新转为 `llm.failed.remote`,进入同一失败级联;abort 由 turn 持有的 AbortController 承载:controller 经 `LlmInput.signal` 传入 request actor,turn 在 `turn.abort` 时直接 abort 它,请求随即以 `llm.failed.remote` 收尾;request actor 不自建 controller、回收时不触碰任何 signal,正常完成的请求绝不可能误 abort 共享 signal。累积器由 turn 持有并随事件流喂入,在 `llm.retrying / llm.recovering` 时 rollback 并重建,每次 attempt 从零累积,从而尽可能保留中断现场(turn 在 `llm.done` 时从累加器 finish 出完整消息)。 +5. **无状态内核 + turn 驱动的编排**。`generate(config, content, control)` 是无状态函数,错误走 onEvent 不 throw;turn machine 直接 invoke 请求 actor(`createRequestActor`):actor 包装单次请求(messageResolvers、abort 作用域、事件 sendBack),turn 借助 retry.ts / recovery.ts 的纯策略函数驱动重试与 recovery:recovery 是一条由调用方组装的策略链(engine 先尝试 `credentialsRecovery`,再尝试配置的媒体降级等替换消息策略),每个策略的纯函数 `propose` 返回自描述记录(`strategy`/`action`、可选替换 `messages`、可选不透明 `beforeRetry` 副作用),turn 执行 `beforeRetry` 和/或替换消息并重进 `thinking`(attempt 重置为 1),重试走 `retrying` 状态的 backoff(尊重 Retry-After),两者分别由 turn 对外补发 `llm.recovering / llm.retrying` 事件;empty response 由 turn 在 `llm.done` 时经纯函数 `emptyResponseError` 判定并重新转为 `llm.failed.remote`,进入同一失败级联;abort 由 turn 持有的 AbortController 承载:controller 经 `LlmInput.signal` 传入 request actor,turn 在 `turn.abort` 时直接 abort 它,请求随即以 `llm.failed.remote` 收尾;request actor 不自建 controller、回收时不触碰任何 signal,正常完成的请求绝不可能误 abort 共享 signal。累积器由 turn 持有并随事件流喂入,在 `llm.retrying / llm.recovering` 时 rollback 并重建,每次 attempt 从零累积,从而尽可能保留中断现场(turn 在 `llm.done` 时从累加器 finish 出完整消息)。 6. **不兜底**。配置是什么就是什么;beta 特性、thinking、empty response 等场景先定义明确报错条件,在请求阶段报错并引导用户修正,而不是静默兜底。 7. **一切可变能力都是贡献点**。provider、媒体上传/降级、usage、traceId、错误恢复(compaction/媒体降级)都通过扩展点接入,llm 内核不含这些概念。 8. **数据即数据**。model 是无函数的纯数据(endpoint url + model 唯一标识一个模型),可序列化、可直接作为 generate 输入;catalog 是 `provider -> models` 的派生缓存,依赖方向只能从 models-dev 指向 llm 内部,不能反向依赖。 @@ -37,7 +37,7 @@ llm/ │ │ LlmRequestConfig.credentials:凭证贡献点 │ │ (resolve/canRecover/invalidate),由调用方在每次 attempt 前解析; │ │ 工厂与 credentialsRecovery 策略位于 human/credentials -│ │ (staticCredentials / oauthCredentials;kimiOAuthCredentialProvider +│ │ (createStaticCredentialProvider / createOAuthCredentialProvider;kimiOAuthCredentialProvider │ │ 适配 Kimi OAuth token);供 direct 调用方使用的 │ │ runWithCredentialRecovery / streamWithCredentialRecovery 执行器 │ │ 位于 llm-adapter/model/credential-recovery @@ -60,7 +60,7 @@ llm/ └── media/ 媒体贡献点:cache / degrade / ref / resolver / store / upload ``` -请求生命周期:`generate` 收到 (config, content, control) → 调用方在每次 attempt 前把 `config.credentials` 解析成带完整凭证的 model(machine 路径由 request actor 完成),请求因此始终携带新鲜凭证,而凭证刷新恢复(可恢复的 401 → `credentials.invalidate()`,以 `llm.recovering`(strategy 为 `credentials`)发出)在重发时自然重新解析(不经状态机的 direct 调用方——ping、generate、full compaction、媒体上传——通过 `runWithCredentialRecovery` / `streamWithCredentialRecovery` 共享同一套单次重试恢复) → requester 的 `plan*` 函数将纯 format 阶段与 trait hooks 组合为协议 requestParams(format 将通用 Message[] 经 Pattern Rewriter 降低,trait 在其间调整 kwargs、转换消息、合并历史、转换 tools 并收尾 params) → internalGenerate 调用官方 SDK → 流式 chunk 经无状态 parser 回调转换为 `llm.streaming.part / streaming.usage / streaming.finish / streaming.message_id` 事件 → 错误由 format 转换为 `llm.failed.*`;成功时 requester 发出 `llm.done`,失败时以 `llm.failed.syntax / llm.failed.remote` 收尾、不再发 `llm.done`。turn 在 `llm.done` 时经 `emptyResponseError` 判定空响应并重新转为 `llm.failed.remote`;turn machine 对 `llm.failed.remote` 先尝试恢复(由 engine 组装的策略链——可恢复 401 的凭证刷新在前、替换消息策略在后——经纯函数 `propose` 产出带不透明 `prepare` 副作用的记录,发 `llm.recovering`),再按策略 backoff 重试(尊重 Retry-After,发 `llm.retrying`),耗尽后才将 turn 置为失败。turn 持有 HistoryAccumulator 随事件流累积,在 `llm.retrying / llm.recovering` 时 rollback 并重建累加器,`llm.done` 时 finish 出完整消息;usage 统计、trace、compaction、媒体降级均以插件/贡献点身份挂接在事件流上。 +请求生命周期:`generate` 收到 (config, content, control) → 调用方在每次 attempt 前把 `config.credentials` 解析成带完整凭证的 model(machine 路径由 request actor 完成),请求因此始终携带新鲜凭证,而凭证刷新恢复(可恢复的 401 → `credentials.invalidate()`,以 `llm.recovering`(strategy 为 `credentials`)发出)在重发时自然重新解析(不经状态机的 direct 调用方——ping、generate、full compaction、媒体上传——通过 `runWithCredentialRecovery` / `streamWithCredentialRecovery` 共享同一套单次重试恢复) → requester 的 `plan*` 函数将纯 format 阶段与 trait hooks 组合为协议 requestParams(format 将通用 Message[] 经 Pattern Rewriter 降低,trait 在其间调整 kwargs、转换消息、合并历史、转换 tools 并收尾 params) → internalGenerate 调用官方 SDK → 流式 chunk 经无状态 parser 回调转换为 `llm.streaming.part / streaming.usage / streaming.finish / streaming.message_id` 事件 → 错误由 format 转换为 `llm.failed.*`;成功时 requester 发出 `llm.done`,失败时以 `llm.failed.syntax / llm.failed.remote` 收尾、不再发 `llm.done`。turn 在 `llm.done` 时经 `emptyResponseError` 判定空响应并重新转为 `llm.failed.remote`;turn machine 对 `llm.failed.remote` 先尝试恢复(由 engine 组装的策略链——可恢复 401 的凭证刷新在前、替换消息策略在后——经纯函数 `propose` 产出带不透明 `beforeRetry` 副作用的记录,发 `llm.recovering`),再按策略 backoff 重试(尊重 Retry-After,发 `llm.retrying`),耗尽后才将 turn 置为失败。turn 持有 HistoryAccumulator 随事件流累积,在 `llm.retrying / llm.recovering` 时 rollback 并重建累加器,`llm.done` 时 finish 出完整消息;usage 统计、trace、compaction、媒体降级均以插件/贡献点身份挂接在事件流上。 ## 已被否决的方案(不要再引入) diff --git a/packages/agent-core-v2/src/human/agent/turn.ts b/packages/agent-core-v2/src/human/agent/turn.ts index 43b6fc684ce..9a8a7ee39a4 100644 --- a/packages/agent-core-v2/src/human/agent/turn.ts +++ b/packages/agent-core-v2/src/human/agent/turn.ts @@ -189,7 +189,7 @@ export type TurnEvent = | { type: 'turn.notify'; messages: HistoryMessage[] } | { type: 'turn.abort' } | { - type: 'turn.failure.triaged'; + type: 'turn.failure.classified'; cause: Extract; proposal?: LlmRecoveryProposal & LlmRecoveryRecord; }; @@ -222,7 +222,7 @@ export interface TurnMachineContext { attempt: number; delayMs: number; appliedRecoveries: LlmRecoveryRecord[]; - recoveryMessages?: readonly Message[]; + attemptMessageOverride?: readonly Message[]; outcome?: 'done' | 'failed' | 'aborted'; error?: unknown; } @@ -286,7 +286,7 @@ function baseMessages(context: TurnMachineContext): readonly Message[] { } function attemptMessages(context: TurnMachineContext): readonly Message[] { - return context.recoveryMessages ?? baseMessages(context); + return context.attemptMessageOverride ?? baseMessages(context); } function proposeRecovery( @@ -579,7 +579,7 @@ export function createTurnMachine( }, 'llm.failed.remote': { actions: raise(({ context, event }) => ({ - type: 'turn.failure.triaged' as const, + type: 'turn.failure.classified' as const, cause: event, proposal: proposeRecovery(recovery, { error: event.error, @@ -589,7 +589,7 @@ export function createTurnMachine( }), })), }, - 'turn.failure.triaged': [ + 'turn.failure.classified': [ { guard: ({ event }) => event.proposal !== undefined, target: 'thinking', @@ -597,7 +597,7 @@ export function createTurnMachine( actions: [ ({ context, event }) => { context.accumulator.rollback(); - event.proposal?.prepare?.(); + event.proposal?.beforeRetry?.(); }, assign(({ context, event }) => { const proposal = event.proposal as LlmRecoveryProposal & LlmRecoveryRecord; @@ -606,7 +606,7 @@ export function createTurnMachine( ...context.appliedRecoveries, { strategy: proposal.strategy, action: proposal.action }, ], - recoveryMessages: proposal.messages ?? context.recoveryMessages, + attemptMessageOverride: proposal.messages ?? context.attemptMessageOverride, attempt: 1, }; }), @@ -794,7 +794,7 @@ export function createTurnMachine( steps: event.messages.length > 0 ? 1 : context.steps + 1, attempt: 1, appliedRecoveries: [], - recoveryMessages: undefined, + attemptMessageOverride: undefined, })), 'signalRemindersConsumed', ], diff --git a/packages/agent-core-v2/src/human/credentials/credentials.ts b/packages/agent-core-v2/src/human/credentials/credentials.ts index 8a89d18ed87..66671e66961 100644 --- a/packages/agent-core-v2/src/human/credentials/credentials.ts +++ b/packages/agent-core-v2/src/human/credentials/credentials.ts @@ -7,18 +7,20 @@ import { type LlmCredentialProvider, } from '#/llm/requester/requester'; -export interface CredentialTokenSource { +export interface AccessTokenResolver { (options?: { readonly force?: boolean }): Promise; } -export function staticCredentials(apiKey?: string): LlmCredentialProvider { +export function createStaticCredentialProvider(apiKey?: string): LlmCredentialProvider { return { resolve: () => apiKey === undefined || apiKey.trim().length === 0 ? undefined : { apiKey }, }; } -export function oauthCredentials(getToken: CredentialTokenSource): LlmCredentialProvider { +export function createOAuthCredentialProvider( + getToken: AccessTokenResolver, +): LlmCredentialProvider { let refreshed: Promise | undefined; return { resolve: async () => { @@ -69,7 +71,7 @@ export const credentialsRecovery: LlmRecovery = { return { strategy: CREDENTIALS_RECOVERY_ID, action: 'refresh', - prepare: () => credentials?.invalidate?.(), + beforeRetry: () => credentials?.invalidate?.(), }; }, }; diff --git a/packages/agent-core-v2/src/human/credentials/kimi-oauth.ts b/packages/agent-core-v2/src/human/credentials/kimi-oauth.ts index 091e5b804d5..0e5c13750bb 100644 --- a/packages/agent-core-v2/src/human/credentials/kimi-oauth.ts +++ b/packages/agent-core-v2/src/human/credentials/kimi-oauth.ts @@ -1,8 +1,8 @@ import type { BearerTokenProvider } from '@moonshot-ai/kimi-code-oauth'; -import { oauthCredentials } from '#/credentials/credentials'; +import { createOAuthCredentialProvider } from '#/credentials/credentials'; import type { LlmCredentialProvider } from '#/llm/requester/requester'; export function kimiOAuthCredentialProvider(tokens: BearerTokenProvider): LlmCredentialProvider { - return oauthCredentials((options) => tokens.getAccessToken(options)); + return createOAuthCredentialProvider((options) => tokens.getAccessToken(options)); } diff --git a/packages/agent-core-v2/src/human/llm/provider/definition.ts b/packages/agent-core-v2/src/human/llm/provider/definition.ts index 52b1bf7273f..c64f70b0b75 100644 --- a/packages/agent-core-v2/src/human/llm/provider/definition.ts +++ b/packages/agent-core-v2/src/human/llm/provider/definition.ts @@ -9,18 +9,18 @@ import type { OpenAIResponsesTrait } from '#/llm/requester/bases/openai-response import type { OpenAITrait } from '#/llm/requester/bases/openai/trait'; import type { LlmErrorClassifier, LlmRequester } from '#/llm/requester/requester'; -export interface ProtocolTraitMap { +export interface ProtocolTraitsByName { readonly openai: OpenAITrait; readonly openai_responses: OpenAIResponsesTrait; readonly anthropic: AnthropicTrait; readonly 'google-genai': GoogleGenAITrait; } -export type AnyProtocolTrait = ProtocolTraitMap[ProtocolName]; +export type ProtocolTraitFor = ProtocolTraitsByName[N]; -export interface ProtocolVariant { - readonly base: ProtocolBase; - readonly trait?: ProtocolTraitMap[N]; +export interface ProtocolBinding { + readonly base: ProtocolBase>; + readonly trait?: ProtocolTraitFor; readonly connection?: ProviderConnection; readonly convertError?: LlmErrorClassifier; readonly capability?: (modelName: string) => ModelCapability | undefined; @@ -38,7 +38,7 @@ export type ProviderModelSource = () => Promise; export interface ProviderDefinition { readonly id: string; - readonly protocols: Readonly<{ [N in ProtocolName]?: ProtocolVariant }>; + readonly protocols: Readonly<{ [N in ProtocolName]?: ProtocolBinding }>; readonly media?: ProviderMediaContribution; readonly models?: ProviderModelSource; } @@ -57,21 +57,21 @@ export interface Provider { } export function createProvider(definition: ProviderDefinition): Provider { - const entries = new Map(); + const entries = new Map(); for (const name of Object.keys(definition.protocols) as ProtocolName[]) { const protocol = definition.protocols[name]; if (protocol !== undefined) { entries.set(name, protocol); } } - const defaultVariant = entries.values().next().value; - if (defaultVariant === undefined) { + const defaultBinding = entries.values().next().value; + if (defaultBinding === undefined) { throw new Error(`provider '${definition.id}' declares no protocols`); } - const variantFor = (name: ProtocolName | undefined): ProtocolVariant => { + const bindingFor = (name: ProtocolName | undefined): ProtocolBinding => { if (name === undefined) { - return defaultVariant; + return defaultBinding; } const found = entries.get(name); if (found === undefined) { @@ -82,8 +82,8 @@ export function createProvider(definition: ProviderDefinition): Provider { return found; }; - const detectCapability = (variant: ProtocolVariant, modelName: string): ModelCapability => - variant.capability?.(modelName) ?? variant.base.capability?.(modelName) ?? UNKNOWN_CAPABILITY; + const detectCapability = (binding: ProtocolBinding, modelName: string): ModelCapability => + binding.capability?.(modelName) ?? binding.base.capability?.(modelName) ?? UNKNOWN_CAPABILITY; return { id: definition.id, @@ -98,9 +98,9 @@ export function createProvider(definition: ProviderDefinition): Provider { provider: definition.id, model: seed.model, capability: - defaultVariant.capability?.(seed.model) ?? + defaultBinding.capability?.(seed.model) ?? seed.capability ?? - defaultVariant.base.capability?.(seed.model) ?? + defaultBinding.base.capability?.(seed.model) ?? UNKNOWN_CAPABILITY, maxContextSize: seed.maxContextSize, maxInputSize: seed.maxInputSize, @@ -110,7 +110,7 @@ export function createProvider(definition: ProviderDefinition): Provider { resolveModel: (model, options = {}) => ({ provider: definition.id, model, - capability: detectCapability(variantFor(options.protocol), model), + capability: detectCapability(bindingFor(options.protocol), model), baseUrl: options.baseUrl, apiKey: options.apiKey, defaultHeaders: options.defaultHeaders, @@ -118,11 +118,11 @@ export function createProvider(definition: ProviderDefinition): Provider { vertexai: options.vertexai, }), createRequester: (protocol) => { - const variant = variantFor(protocol); - return variant.base.createRequester({ - connection: variant.connection, - trait: variant.trait, - convertError: variant.convertError, + const binding = bindingFor(protocol); + return binding.base.createRequester({ + connection: binding.connection, + trait: binding.trait, + convertError: binding.convertError, }); }, }; diff --git a/packages/agent-core-v2/src/human/llm/requester/recovery.ts b/packages/agent-core-v2/src/human/llm/requester/recovery.ts index c3275ad5e34..d2f4242fdbf 100644 --- a/packages/agent-core-v2/src/human/llm/requester/recovery.ts +++ b/packages/agent-core-v2/src/human/llm/requester/recovery.ts @@ -17,7 +17,7 @@ export interface LlmRecoveryContext { export interface LlmRecoveryProposal { readonly action: string; readonly messages?: readonly Message[]; - readonly prepare?: () => void; + readonly beforeRetry?: () => void; } export interface LlmRecovery { diff --git a/packages/agent-core-v2/src/human/test/credentials/credentials.test.ts b/packages/agent-core-v2/src/human/test/credentials/credentials.test.ts index bd8f08bcfe6..53397850696 100644 --- a/packages/agent-core-v2/src/human/test/credentials/credentials.test.ts +++ b/packages/agent-core-v2/src/human/test/credentials/credentials.test.ts @@ -3,9 +3,9 @@ import { describe, expect, it } from 'vitest'; import { applyCredential, credentialsRecovery, - oauthCredentials, + createOAuthCredentialProvider, resolveModelCredentials, - staticCredentials, + createStaticCredentialProvider, } from '#/credentials/credentials'; import type { LlmModel } from '#/llm/model'; import type { LlmRecoveryContext, LlmRecoveryRecord } from '#/llm/requester/recovery'; @@ -18,24 +18,24 @@ const MODEL: LlmModel = { defaultHeaders: { 'x-base': '1' }, }; -describe('staticCredentials', () => { +describe('createStaticCredentialProvider', () => { it('resolves the static api key and never recovers', async () => { - const provider = staticCredentials('sk-1'); + const provider = createStaticCredentialProvider('sk-1'); expect(await provider.resolve()).toEqual({ apiKey: 'sk-1' }); expect(provider.canRecover).toBeUndefined(); expect(provider.invalidate).toBeUndefined(); }); it('resolves undefined for missing or blank keys', async () => { - expect(await staticCredentials(undefined).resolve()).toBeUndefined(); - expect(await staticCredentials(' ').resolve()).toBeUndefined(); + expect(await createStaticCredentialProvider(undefined).resolve()).toBeUndefined(); + expect(await createStaticCredentialProvider(' ').resolve()).toBeUndefined(); }); }); -describe('oauthCredentials', () => { +describe('createOAuthCredentialProvider', () => { it('refreshes with force on invalidate and consumes the refresh on the next resolve', async () => { const calls: (boolean | undefined)[] = []; - const provider = oauthCredentials((options) => { + const provider = createOAuthCredentialProvider((options) => { calls.push(options?.force); return Promise.resolve('tok'); }); @@ -51,7 +51,7 @@ describe('oauthCredentials', () => { it('starts the forced refresh eagerly on invalidate, before the next resolve', async () => { const calls: (boolean | undefined)[] = []; - const provider = oauthCredentials((options) => { + const provider = createOAuthCredentialProvider((options) => { calls.push(options?.force); return Promise.resolve('tok'); }); @@ -67,7 +67,7 @@ describe('oauthCredentials', () => { it('coalesces repeated invalidates into a single refresh', async () => { const calls: (boolean | undefined)[] = []; - const provider = oauthCredentials((options) => { + const provider = createOAuthCredentialProvider((options) => { calls.push(options?.force); return Promise.resolve('tok'); }); @@ -81,7 +81,7 @@ describe('oauthCredentials', () => { it('propagates a failed refresh to the consuming resolve and recovers afterwards', async () => { let calls = 0; - const provider = oauthCredentials(() => { + const provider = createOAuthCredentialProvider(() => { calls += 1; return calls === 1 ? Promise.reject(new Error('login required')) : Promise.resolve('tok'); }); @@ -93,7 +93,7 @@ describe('oauthCredentials', () => { }); it('recovers only from 401 errors', () => { - const provider = oauthCredentials(() => Promise.resolve('tok')); + const provider = createOAuthCredentialProvider(() => Promise.resolve('tok')); expect(provider.canRecover?.(Object.assign(new Error('x'), { status: 401 }))).toBe(true); expect(provider.canRecover?.(Object.assign(new Error('x'), { statusCode: 401 }))).toBe(true); expect(provider.canRecover?.(Object.assign(new Error('x'), { statusCode: 403 }))).toBe(false); @@ -101,7 +101,7 @@ describe('oauthCredentials', () => { }); it('resolves undefined when the token source has no token', async () => { - const provider = oauthCredentials(() => Promise.resolve(undefined)); + const provider = createOAuthCredentialProvider(() => Promise.resolve(undefined)); await expect(provider.resolve()).resolves.toBeUndefined(); }); }); @@ -137,15 +137,15 @@ const forbidden = Object.assign(new Error('forbidden'), { status: 403 }); describe('credentialsRecovery', () => { it('proposes a credentials refresh on a recoverable error', () => { - const provider = oauthCredentials(() => Promise.resolve('tok')); + const provider = createOAuthCredentialProvider(() => Promise.resolve('tok')); expect(credentialsRecovery.propose(recoveryContext(unauthorized, [], provider))).toEqual({ strategy: 'credentials', action: 'refresh', - prepare: expect.any(Function), + beforeRetry: expect.any(Function), }); }); - it('invalidates the credentials when the proposal prepares', () => { + it('invalidates the credentials before retrying', () => { let invalidations = 0; const provider: LlmCredentialProvider = { resolve: () => ({ apiKey: 'tok' }), @@ -155,24 +155,32 @@ describe('credentialsRecovery', () => { }, }; const proposal = credentialsRecovery.propose(recoveryContext(unauthorized, [], provider)); - proposal?.prepare?.(); + proposal?.beforeRetry?.(); expect(invalidations).toBe(1); }); it('does not propose when the strategy was already applied', () => { - const provider = oauthCredentials(() => Promise.resolve('tok')); + const provider = createOAuthCredentialProvider(() => Promise.resolve('tok')); const applied: LlmRecoveryRecord[] = [{ strategy: 'credentials', action: 'refresh' }]; - expect(credentialsRecovery.propose(recoveryContext(unauthorized, applied, provider))).toBeUndefined(); + expect( + credentialsRecovery.propose(recoveryContext(unauthorized, applied, provider)), + ).toBeUndefined(); }); it('does not propose without recoverable credentials', () => { expect(credentialsRecovery.propose(recoveryContext(unauthorized))).toBeUndefined(); expect( - credentialsRecovery.propose(recoveryContext(unauthorized, [], staticCredentials('sk-1'))), + credentialsRecovery.propose( + recoveryContext(unauthorized, [], createStaticCredentialProvider('sk-1')), + ), ).toBeUndefined(); expect( credentialsRecovery.propose( - recoveryContext(forbidden, [], oauthCredentials(() => Promise.resolve('tok'))), + recoveryContext( + forbidden, + [], + createOAuthCredentialProvider(() => Promise.resolve('tok')), + ), ), ).toBeUndefined(); }); diff --git a/packages/agent-core-v2/src/llm-adapter/model/catalog-service.ts b/packages/agent-core-v2/src/llm-adapter/model/catalog-service.ts index 9b3e724cd10..ba42bd95338 100644 --- a/packages/agent-core-v2/src/llm-adapter/model/catalog-service.ts +++ b/packages/agent-core-v2/src/llm-adapter/model/catalog-service.ts @@ -6,7 +6,10 @@ import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { Error2 } from '#/_base/errors/errors'; import type { CatalogModel, CatalogProviderInfo } from '#human/llm/provider-catalog'; -import { oauthCredentials, staticCredentials } from '#human/credentials/credentials'; +import { + createOAuthCredentialProvider, + createStaticCredentialProvider, +} from '#human/credentials/credentials'; import type { LlmCredentialProvider } from '#human/llm/requester/requester'; import type { ModelCapability } from '../contract/capability'; import { CONFIG_INVALID_ERROR_CODE } from '../contract/errors'; @@ -450,17 +453,17 @@ export class ModelCatalog extends Disposable implements IModelCatalog { auth: ResolvedModelAuthMaterial, ): LlmCredentialProvider { if (auth.apiKey !== undefined) { - return staticCredentials(auth.apiKey); + return createStaticCredentialProvider(auth.apiKey); } if (auth.oauth !== undefined) { const oauthRef = auth.oauth; const providerKey = auth.oauthProviderKey ?? providerName; const tokens = this.oauth; - return oauthCredentials((options) => + return createOAuthCredentialProvider((options) => tokens.getAccessToken(providerKey, oauthRef, { force: options?.force === true }), ); } - return staticCredentials(undefined); + return createStaticCredentialProvider(undefined); } } diff --git a/packages/agent-core-v2/src/llm-adapter/protocol/protocol-base.ts b/packages/agent-core-v2/src/llm-adapter/protocol/protocol-base.ts index 5d7b5ecc010..d9ae1282e1a 100644 --- a/packages/agent-core-v2/src/llm-adapter/protocol/protocol-base.ts +++ b/packages/agent-core-v2/src/llm-adapter/protocol/protocol-base.ts @@ -1,5 +1,5 @@ import type { ProtocolBase } from '#human/llm/protocol/base'; -import type { AnyProtocolTrait } from '#human/llm/provider/definition'; +import type { ProtocolTraitFor } from '#human/llm/provider/definition'; import { anthropicBase } from '#human/llm/requester/bases/anthropic/requester'; import { googleGenAIBase } from '#human/llm/requester/bases/google-genai/requester'; import { openAIBase } from '#human/llm/requester/bases/openai/requester'; @@ -11,12 +11,12 @@ export type ProtocolBaseId = Protocol; export interface ProtocolBaseDefinition { readonly id: ProtocolBaseId; - readonly base: ProtocolBase; + readonly base: ProtocolBase>; } export interface ResolvedAdapterIdentity { readonly baseId: ProtocolBaseId; - readonly trait?: AnyProtocolTrait; + readonly trait?: ProtocolTraitFor; } const PROTOCOL_BASES: readonly ProtocolBaseDefinition[] = [ diff --git a/packages/agent-core-v2/src/llm-adapter/protocol/protocolAdapterRegistry.ts b/packages/agent-core-v2/src/llm-adapter/protocol/protocolAdapterRegistry.ts index 2e083fb5765..1936d25f105 100644 --- a/packages/agent-core-v2/src/llm-adapter/protocol/protocolAdapterRegistry.ts +++ b/packages/agent-core-v2/src/llm-adapter/protocol/protocolAdapterRegistry.ts @@ -7,7 +7,7 @@ import type { ProviderMediaContribution } from '#human/llm/media/upload'; import type { LlmModel } from '#human/llm/model'; import type { ProtocolBase } from '#human/llm/protocol/base'; import type { ProviderConnection } from '#human/llm/protocol/connection'; -import type { AnyProtocolTrait } from '#human/llm/provider/definition'; +import type { ProtocolTraitFor } from '#human/llm/provider/definition'; import type { LlmErrorClassifier } from '#human/llm/requester/requester'; import { anthropicBase, anthropicBetaBase } from '#human/llm/requester/bases/anthropic/requester'; import { @@ -45,8 +45,8 @@ const kimiMedia: ProviderMediaContribution = { }; interface AdapterRoute { - readonly base: ProtocolBase; - readonly trait?: AnyProtocolTrait; + readonly base: ProtocolBase>; + readonly trait?: ProtocolTraitFor; readonly connection?: ProviderConnection; readonly convertError?: LlmErrorClassifier; readonly providerId: string; diff --git a/packages/agent-core-v2/src/llm-adapter/provider/provider-definition.ts b/packages/agent-core-v2/src/llm-adapter/provider/provider-definition.ts index 82cb0e4534d..9c8c6e3f8c1 100644 --- a/packages/agent-core-v2/src/llm-adapter/provider/provider-definition.ts +++ b/packages/agent-core-v2/src/llm-adapter/provider/provider-definition.ts @@ -1,7 +1,7 @@ import { BugIndicatingError } from '#/_base/errors/errors'; import type { ModelCapability as HumanModelCapability } from '#human/llm/capability'; import type { ProtocolEndpoint, ProviderConnection } from '#human/llm/protocol/connection'; -import type { ProtocolTraitMap } from '#human/llm/provider/definition'; +import type { ProtocolTraitFor } from '#human/llm/provider/definition'; import type { LlmErrorClassifier } from '#human/llm/requester/requester'; import { kimiAnthropicTrait, @@ -49,7 +49,7 @@ export const kimiEndpoint: ProtocolEndpoint = { export interface ProviderDefinition { readonly id: string; readonly baseProtocol: N; - readonly trait?: ProtocolTraitMap[N]; + readonly trait?: ProtocolTraitFor; readonly connection?: ProviderConnection; readonly convertError?: LlmErrorClassifier; readonly capability?: (modelName: string) => HumanModelCapability | undefined; diff --git a/packages/agent-core-v2/test/llm-adapter/model/modelRequester.test.ts b/packages/agent-core-v2/test/llm-adapter/model/modelRequester.test.ts index 311601f6d0b..15970a18b2d 100644 --- a/packages/agent-core-v2/test/llm-adapter/model/modelRequester.test.ts +++ b/packages/agent-core-v2/test/llm-adapter/model/modelRequester.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from 'vitest'; import { isError2 } from '#/_base/errors/errors'; -import { oauthCredentials, staticCredentials } from '#human/credentials/credentials'; +import { + createOAuthCredentialProvider, + createStaticCredentialProvider, +} from '#human/credentials/credentials'; import type { ProviderMediaContribution } from '#human/llm/media/upload'; import type { LlmModel } from '#human/llm/model'; import type { @@ -119,7 +122,10 @@ describe('ModelRequesterImpl request execution', () => { it('maps ModelRequestParams onto LlmRequestConfig, content and control', async () => { const requester = new FakeLlmRequester(); requester.handler = (_i, emit) => textStream(emit); - const impl = new ModelRequesterImpl(modelWith(staticCredentials('sk-1')), gatewayReturning(requester)); + const impl = new ModelRequesterImpl( + modelWith(createStaticCredentialProvider('sk-1')), + gatewayReturning(requester), + ); const signal = AbortSignal.timeout(1000); const messages: Message[] = [ { @@ -194,7 +200,10 @@ describe('ModelRequesterImpl request execution', () => { it('omits the thinking intent when no effort is requested', async () => { const requester = new FakeLlmRequester(); requester.handler = (_i, emit) => textStream(emit); - const impl = new ModelRequesterImpl(modelWith(staticCredentials()), gatewayReturning(requester)); + const impl = new ModelRequesterImpl( + modelWith(createStaticCredentialProvider()), + gatewayReturning(requester), + ); await collect(impl.request(INPUT)); expect(requester.calls[0]?.config.thinking).toBeUndefined(); expect(requester.calls[0]?.config.extraParams).toBeUndefined(); @@ -221,7 +230,10 @@ describe('ModelRequesterImpl request execution', () => { { type: 'llm.done' }, ]); const traceIds: Array = []; - const impl = new ModelRequesterImpl(modelWith(staticCredentials()), gatewayReturning(requester)); + const impl = new ModelRequesterImpl( + modelWith(createStaticCredentialProvider()), + gatewayReturning(requester), + ); const events = await collect( impl.request(INPUT, undefined, { onTraceId: (id) => traceIds.push(id) }), ); @@ -272,7 +284,7 @@ describe('ModelRequesterImpl request execution', () => { }, }); const impl = new ModelRequesterImpl( - modelWith(oauthCredentials(() => Promise.resolve('tok'))), + modelWith(createOAuthCredentialProvider(() => Promise.resolve('tok'))), gatewayReturning(requester), ); @@ -298,7 +310,7 @@ describe('ModelRequesterImpl request execution', () => { }, }); const impl = new ModelRequesterImpl( - modelWith(staticCredentials('sk-bad')), + modelWith(createStaticCredentialProvider('sk-bad')), gatewayReturning(requester), ); @@ -321,7 +333,10 @@ describe('ModelRequesterImpl request execution', () => { headers: null, }, }); - const impl = new ModelRequesterImpl(modelWith(staticCredentials()), gatewayReturning(requester)); + const impl = new ModelRequesterImpl( + modelWith(createStaticCredentialProvider()), + gatewayReturning(requester), + ); const failure = await collect(impl.request(INPUT)).catch((error: unknown) => error); expect((failure as { code: string }).code).toBe(PROVIDER_API_ERROR_CODE); @@ -348,7 +363,7 @@ describe('ModelRequesterImpl request execution', () => { it('uploadVideo presence is the capability declaration', async () => { const requester = new FakeLlmRequester(); const impl = new ModelRequesterImpl( - modelWith(staticCredentials('sk-1')), + modelWith(createStaticCredentialProvider('sk-1')), gatewayReturning(requester), ); await expect(impl.uploadVideo('file-id')).rejects.toThrow(/does not support video upload/); @@ -364,7 +379,7 @@ describe('ModelRequesterImpl request execution', () => { }, }; const withMedia = new ModelRequesterImpl( - modelWith(staticCredentials('sk-1')), + modelWith(createStaticCredentialProvider('sk-1')), gatewayReturning(requester, media), ); const part = await withMedia.uploadVideo({ data: new Uint8Array([1]), mimeType: 'video/mp4' }); @@ -383,7 +398,10 @@ describe('ModelRequesterImpl request execution', () => { emit({ type: 'llm.streaming.finish', finish: { finishReason: 'completed', rawFinishReason: 'stop' } }); emit({ type: 'llm.done' }); }; - const impl = new ModelRequesterImpl(modelWith(staticCredentials()), gatewayReturning(requester)); + const impl = new ModelRequesterImpl( + modelWith(createStaticCredentialProvider()), + gatewayReturning(requester), + ); const events = await collect(impl.request(INPUT)); const timing = events.find((event) => event.type === 'timing'); expect(timing).toBeDefined(); diff --git a/packages/klient/examples/kimi-select-tools.ts b/packages/klient/examples/kimi-select-tools.ts index a791d780e97..28cf7ccc211 100644 --- a/packages/klient/examples/kimi-select-tools.ts +++ b/packages/klient/examples/kimi-select-tools.ts @@ -63,7 +63,7 @@ import { renderLoadableToolsAnnouncement } from '@moonshot-ai/agent-core-v2/agen import { UNKNOWN_CAPABILITY } from '@moonshot-ai/agent-core-v2/llm-adapter/contract/capability'; import type { Message } from '@moonshot-ai/agent-core-v2/llm-adapter/contract/message'; import type { ToolDescription as Tool } from '@moonshot-ai/agent-core-v2/human/llm/message'; -import { staticCredentials } from '@moonshot-ai/agent-core-v2/human/credentials/credentials'; +import { createStaticCredentialProvider } from '@moonshot-ai/agent-core-v2/human/credentials/credentials'; import type { Model } from '@moonshot-ai/agent-core-v2/llm-adapter/model/catalog'; import { IModelCatalog } from '@moonshot-ai/agent-core-v2/llm-adapter/model/catalog'; import type { @@ -295,7 +295,7 @@ async function probeWireEncoding(): Promise { alwaysThinking: false, providerType, providerName: providerType ?? 'probe', - credentials: staticCredentials('sk-probe'), + credentials: createStaticCredentialProvider('sk-probe'), }; return new ModelRequesterImpl(model, registry); }; diff --git a/packages/klient/examples/model-requester-boundary.ts b/packages/klient/examples/model-requester-boundary.ts index 46f24bb806b..20b5325faa9 100644 --- a/packages/klient/examples/model-requester-boundary.ts +++ b/packages/klient/examples/model-requester-boundary.ts @@ -60,8 +60,8 @@ import { isToolExchangeAdjacencyError, } from '@moonshot-ai/agent-core-v2/llm-adapter/contract/errors'; import { - oauthCredentials, - staticCredentials, + createOAuthCredentialProvider, + createStaticCredentialProvider, } from '@moonshot-ai/agent-core-v2/human/credentials/credentials'; import type { ToolCall, @@ -389,7 +389,7 @@ async function probeBoundaries(): Promise { // 1) happy path — the requester's event envelope on top of the raw stream. resetCounts(); handler = (_req, res) => writePong(res); - const ok = await collect(makeRequester(staticCredentials('sk-probe'))); + const ok = await collect(makeRequester(createStaticCredentialProvider('sk-probe'))); assert(ok.text === 'pong', 'happy path assembles streamed text'); assert(ok.events.includes('usage'), 'happy path emits a usage event'); assert(ok.events.includes('finish'), 'happy path emits a finish event'); @@ -402,7 +402,7 @@ async function probeBoundaries(): Promise { resetCounts(); handler = (_req, res) => writeJsonError(res, 401, 'invalid api key'); try { - await collect(makeRequester(staticCredentials('sk-bad'))); + await collect(makeRequester(createStaticCredentialProvider('sk-bad'))); throw new Error('expected a failure'); } catch (error) { const { outcome, wrappedBy } = describeCaught(error); @@ -420,7 +420,7 @@ async function probeBoundaries(): Promise { else writeJsonError(res, 401, 'token expired'); }; let resolveCalls = 0; - const refreshable = oauthCredentials((options) => { + const refreshable = createOAuthCredentialProvider((options) => { resolveCalls += 1; return Promise.resolve(options?.force === true ? 'sk-good' : 'sk-stale'); }); @@ -448,7 +448,7 @@ async function probeBoundaries(): Promise { resetCounts(); handler = (_req, res) => writeJsonError(res, 429, 'too many requests', { 'retry-after': '2' }); try { - await collect(makeRequester(staticCredentials('sk-probe'))); + await collect(makeRequester(createStaticCredentialProvider('sk-probe'))); throw new Error('expected a failure'); } catch (error) { const { outcome, wrappedBy } = describeCaught(error); @@ -463,7 +463,7 @@ async function probeBoundaries(): Promise { handler = (_req, res) => writeJsonError(res, 400, 'This model\'s maximum context length is 8192 tokens.'); try { - await collect(makeRequester(staticCredentials('sk-probe'))); + await collect(makeRequester(createStaticCredentialProvider('sk-probe'))); throw new Error('expected a failure'); } catch (error) { const { outcome, wrappedBy } = describeCaught(error); @@ -479,7 +479,7 @@ async function probeBoundaries(): Promise { res.end('500 Internal Server Erroroops'); }; try { - await collect(makeRequester(staticCredentials('sk-probe'))); + await collect(makeRequester(createStaticCredentialProvider('sk-probe'))); throw new Error('expected a failure'); } catch (error) { const { outcome, wrappedBy } = describeCaught(error); @@ -500,7 +500,7 @@ async function probeBoundaries(): Promise { }); handler = (_req, res) => writePong(res); // unused — nothing listens there try { - await collect(makeRequester(staticCredentials('sk-probe'), `http://127.0.0.1:${String(deadPort)}`)); + await collect(makeRequester(createStaticCredentialProvider('sk-probe'), `http://127.0.0.1:${String(deadPort)}`)); throw new Error('expected a failure'); } catch (error) { const { outcome, wrappedBy } = describeCaught(error); @@ -512,7 +512,7 @@ async function probeBoundaries(): Promise { resetCounts(); handler = (_req, res) => writeSse(res, []); try { - await collect(makeRequester(staticCredentials('sk-probe'))); + await collect(makeRequester(createStaticCredentialProvider('sk-probe'))); throw new Error('expected a failure'); } catch (error) { const { outcome, wrappedBy } = describeCaught(error); @@ -528,7 +528,7 @@ async function probeBoundaries(): Promise { res.end('data: {this is not json}\n\ndata: [DONE]\n\n'); }; try { - await collect(makeRequester(staticCredentials('sk-probe'))); + await collect(makeRequester(createStaticCredentialProvider('sk-probe'))); throw new Error('expected a failure'); } catch (error) { const { outcome, wrappedBy } = describeCaught(error); @@ -545,7 +545,7 @@ async function probeBoundaries(): Promise { }); }; try { - await collect(makeRequester(staticCredentials('sk-probe'))); + await collect(makeRequester(createStaticCredentialProvider('sk-probe'))); throw new Error('expected a failure'); } catch (error) { const { outcome, wrappedBy } = describeCaught(error); @@ -578,7 +578,7 @@ async function probeBoundaries(): Promise { sseToolDelta([], 'tool_calls'), SSE_USAGE, ]); - const toolOk = await collect(makeRequester(staticCredentials('sk-probe')), undefined, TOOL_INPUT); + const toolOk = await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_INPUT); const wireTools = (lastRequestBody as { tools?: { function?: { name?: string } }[] }).tools; assert( wireTools?.some((t) => t.function?.name === 'get_weather') === true, @@ -617,7 +617,7 @@ async function probeBoundaries(): Promise { sseToolDelta([], 'tool_calls'), SSE_USAGE, ]); - const parallel = await collect(makeRequester(staticCredentials('sk-probe')), undefined, TOOL_INPUT); + const parallel = await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_INPUT); assert(parallel.toolCalls.length === 2, 'two parallel tool calls assembled'); assert( parallel.toolCalls[0]?.name === 'tool_a' && parallel.toolCalls[0]?.arguments === '{"a":1}', @@ -651,7 +651,7 @@ async function probeBoundaries(): Promise { sseToolDelta([], 'tool_calls'), SSE_USAGE, ]); - const malformedArgs = await collect(makeRequester(staticCredentials('sk-probe')), undefined, TOOL_INPUT); + const malformedArgs = await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_INPUT); assert( malformedArgs.toolCalls[0]?.arguments === '{not json', 'malformed arguments pass through untouched', @@ -676,7 +676,7 @@ async function probeBoundaries(): Promise { sseToolDelta([], 'tool_calls'), SSE_USAGE, ]); - const indexless = await collect(makeRequester(staticCredentials('sk-probe')), undefined, TOOL_INPUT); + const indexless = await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_INPUT); assert( indexless.toolCalls[0]?.arguments === '{"location":"HZ"}', 'index-less fragments merge into the pending call', @@ -694,7 +694,7 @@ async function probeBoundaries(): Promise { handler = (_req, res) => writeJsonError(res, 400, 'tool_call_id "call_1" is not found'); try { - await collect(makeRequester(staticCredentials('sk-probe')), undefined, TOOL_HISTORY_INPUT); + await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_HISTORY_INPUT); throw new Error('expected a failure'); } catch (error) { const { outcome, wrappedBy } = describeCaught(error); @@ -710,7 +710,7 @@ async function probeBoundaries(): Promise { // the tool result must hit the wire in the provider's shape. resetCounts(); handler = (_req, res) => writePong(res); - await collect(makeRequester(staticCredentials('sk-probe')), undefined, TOOL_HISTORY_INPUT); + await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_HISTORY_INPUT); const wireMessages = (lastRequestBody as { messages?: Record[] }).messages; assert( wireMessages?.some( @@ -740,7 +740,7 @@ async function probeBoundaries(): Promise { }; const ac = new AbortController(); try { - for await (const event of makeRequester(staticCredentials('sk-probe')).request(PING_INPUT, ac.signal)) { + for await (const event of makeRequester(createStaticCredentialProvider('sk-probe')).request(PING_INPUT, ac.signal)) { if (event.type === 'part') ac.abort(); } throw new Error('expected an abort');