@cloc/provider-ai-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent.d.ts +93 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +359 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/config.d.ts +85 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +101 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/gateway.d.ts +74 -0
  11. package/dist/gateway.d.ts.map +1 -0
  12. package/dist/gateway.js +96 -0
  13. package/dist/gateway.js.map +1 -0
  14. package/dist/index.d.ts +47 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +46 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/memory-tool.d.ts +63 -0
  19. package/dist/memory-tool.d.ts.map +1 -0
  20. package/dist/memory-tool.js +183 -0
  21. package/dist/memory-tool.js.map +1 -0
  22. package/dist/output.d.ts +49 -0
  23. package/dist/output.d.ts.map +1 -0
  24. package/dist/output.js +41 -0
  25. package/dist/output.js.map +1 -0
  26. package/dist/plugin.d.ts +74 -0
  27. package/dist/plugin.d.ts.map +1 -0
  28. package/dist/plugin.js +86 -0
  29. package/dist/plugin.js.map +1 -0
  30. package/dist/request.d.ts +82 -0
  31. package/dist/request.d.ts.map +1 -0
  32. package/dist/request.js +80 -0
  33. package/dist/request.js.map +1 -0
  34. package/dist/safety.d.ts +54 -0
  35. package/dist/safety.d.ts.map +1 -0
  36. package/dist/safety.js +0 -0
  37. package/dist/safety.js.map +1 -0
  38. package/dist/secrets.d.ts +51 -0
  39. package/dist/secrets.d.ts.map +1 -0
  40. package/dist/secrets.js +47 -0
  41. package/dist/secrets.js.map +1 -0
  42. package/dist/skills-loader.d.ts +76 -0
  43. package/dist/skills-loader.d.ts.map +1 -0
  44. package/dist/skills-loader.js +99 -0
  45. package/dist/skills-loader.js.map +1 -0
  46. package/dist/stream.d.ts +58 -0
  47. package/dist/stream.d.ts.map +1 -0
  48. package/dist/stream.js +59 -0
  49. package/dist/stream.js.map +1 -0
  50. package/dist/tokens.d.ts +17 -0
  51. package/dist/tokens.d.ts.map +1 -0
  52. package/dist/tokens.js +17 -0
  53. package/dist/tokens.js.map +1 -0
  54. package/dist/tool-loop.d.ts +98 -0
  55. package/dist/tool-loop.d.ts.map +1 -0
  56. package/dist/tool-loop.js +210 -0
  57. package/dist/tool-loop.js.map +1 -0
  58. package/dist/trace.d.ts +78 -0
  59. package/dist/trace.d.ts.map +1 -0
  60. package/dist/trace.js +39 -0
  61. package/dist/trace.js.map +1 -0
  62. package/dist/validate.d.ts +54 -0
  63. package/dist/validate.d.ts.map +1 -0
  64. package/dist/validate.js +81 -0
  65. package/dist/validate.js.map +1 -0
  66. package/package.json +55 -0
  67. package/src/agent.ts +487 -0
  68. package/src/config.ts +147 -0
  69. package/src/gateway.ts +126 -0
  70. package/src/index.ts +101 -0
  71. package/src/memory-tool.ts +219 -0
  72. package/src/output.ts +67 -0
  73. package/src/plugin.ts +123 -0
  74. package/src/request.ts +178 -0
  75. package/src/safety.ts +0 -0
  76. package/src/secrets.ts +71 -0
  77. package/src/skills-loader.ts +153 -0
  78. package/src/stream.ts +80 -0
  79. package/src/tokens.ts +82 -0
  80. package/src/tool-loop.ts +268 -0
  81. package/src/trace.ts +87 -0
  82. package/src/validate.ts +118 -0
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@cloc/provider-ai-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Default AgentProvider plugin, backed by the Vercel AI SDK v6 (+ AI Gateway, hosted-first). The model is one config field of the Agent, never the contract. Siblings: provider-ai-langchain, provider-ai-claude, provider-ai-open-ai.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "cloc": {
19
+ "name": "@cloc/provider-ai-sdk",
20
+ "provides": [
21
+ "@cloc/core:agent-provider"
22
+ ],
23
+ "needs": {
24
+ "net": [
25
+ "ai-gateway.vercel.sh"
26
+ ],
27
+ "secrets": [
28
+ "AI_GATEWAY_API_KEY"
29
+ ]
30
+ }
31
+ },
32
+ "dependencies": {
33
+ "ai": "^6.0.34",
34
+ "unstorage": "^1.16.0",
35
+ "zod": "^4.0.0",
36
+ "@cloc/plugin": "0.1.0",
37
+ "@cloc/core": "0.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^24.12.4",
41
+ "typescript": "^5.7.0",
42
+ "vitest": "^3.0.0"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc -p tsconfig.json",
49
+ "typecheck": "tsc --noEmit -p tsconfig.json",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest"
52
+ },
53
+ "main": "./dist/index.js",
54
+ "types": "./dist/index.d.ts"
55
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,487 @@
1
+ /**
2
+ * @cloc/provider-ai-sdk · agent.ts — `AiSdkAgent`, the default AgentProvider over AI SDK v6
3
+ * (FR-002, FR-003, FR-004, data-model §1, contracts/agent-provider.ts).
4
+ *
5
+ * Implements the vendor-free core contract `generate(p, o?) → Promise<Output>` and
6
+ * `stream(p, o?) → AsyncIterable<Delta>` by composing: gateway routing + failover (gateway.ts),
7
+ * the budgeted render-time tool loop (tool-loop.ts), the `Output.object` structured path
8
+ * (output.ts), the validate-or-repair boundary (validate.ts), partial-object streaming (stream.ts),
9
+ * the `agent.generate` trace subtree (trace.ts), and data-not-instructions safety (safety.ts).
10
+ *
11
+ * v2 (027-agentic-primitives §16b): `generate()`/`stream()` may run a BUDGETED AI SDK 6
12
+ * `ToolLoopAgent` loop (`call → execute-tools → feed-results → repeat`) that surfaces the three
13
+ * OPTIONAL, GATED primitives from `GenOpts` — Skills (progressive disclosure; only level-1 metadata
14
+ * enters the prompt), Memory (the Anthropic memory-tool interface, `unstorage`-backed), and Tools
15
+ * (the three sources, joined to the loop). The loop is bounded by `stopWhen` (the SAME trajectory
16
+ * budget as the `0` console agent, §9.1) and shaped per-step by `prepareStep`; EVERY tool/skill/
17
+ * memory access clears the §58 policy gate BEFORE execution, and a denial DEGRADES (FR-014/FR-021).
18
+ * When none of the primitives are set the render is the plain pre-027 path (FR-002, zero added cost).
19
+ *
20
+ * No AI SDK / AI Gateway type crosses the public signature — they live ONLY inside this package
21
+ * (FR-002, Principle 8). The model is a swappable FIELD (GenOpts.model), never the contract.
22
+ *
23
+ * AI SDK v6 surface (verified against the installed ai@6.0.195 .d.ts, not stale memory):
24
+ * - `generateText({ model, output: Output.object({ schema }), prompt|messages, tools, stopWhen,
25
+ * prepareStep, maxOutputTokens, seed, abortSignal })` → `result.output` (typed; the older
26
+ * `result.experimental_output` is now `@deprecated` — we read the stable name first and fall
27
+ * back to the experimental/`output`-less alias so a rename in EITHER direction can't crash us).
28
+ * - `streamText({ ...same... })` → `result.partialOutputStream` (the `experimental_*` alias is
29
+ * `@deprecated`), `result.output` (a Promise of the final typed object), `result.fullStream`.
30
+ * - `maxTokens` is `maxOutputTokens`; `maxSteps` is `stopWhen: stepCountIs(n)`; the other stop
31
+ * conditions are `hasToolCall(name)` / `isLoopFinished()`; `prepareStep({ stepNumber, steps,
32
+ * model, messages }) => { model?, activeTools?, toolChoice? }` does per-step engineering;
33
+ * `generateObject` is deprecated in favor of `output: Output.object(...)` (FR-013).
34
+ * - `result.usage` is a `LanguageModelUsage` (`{ inputTokens, outputTokens, totalTokens,
35
+ * inputTokenDetails.cacheReadTokens, ... }`); on the streaming result it is a `Promise`.
36
+ */
37
+
38
+ import { generateText, streamText } from "ai";
39
+ import type { ToolSet as AiToolSet } from "ai";
40
+ import type { AgentProvider, GenOpts, Prompt, Output, Delta } from "./tokens.js";
41
+ import type { PolicyGateHook, StepDirective } from "./tokens.js";
42
+ import { ALLOW_ALL_GATE } from "./tokens.js";
43
+ import { modelRefToString } from "@cloc/core";
44
+ import type { AgentConfig } from "./config.js";
45
+ import { resolveGateway, withFailover, AgentError, type ResolvedGateway } from "./gateway.js";
46
+ import { type SecretHandle, resolveGatewayKey } from "./secrets.js";
47
+ import { outputSpecFor, makeStructuredOutput, type StructuredOutput } from "./output.js";
48
+ import { validateOrRepair } from "./validate.js";
49
+ import {
50
+ buildToolSet,
51
+ buildAgenticToolSet,
52
+ toAiStopWhen,
53
+ toAiPrepareStep,
54
+ type LoopEvent,
55
+ type PrepareStepResultModel,
56
+ } from "./tool-loop.js";
57
+ import { buildMemoryTools } from "./memory-tool.js";
58
+ import { frameSkillsForPrompt } from "./skills-loader.js";
59
+ import { frameGroundingAsData, collectProvenance } from "./safety.js";
60
+ import {
61
+ startAgentSpan,
62
+ type SpanSink,
63
+ type AgentGenerateContext,
64
+ } from "./trace.js";
65
+ import {
66
+ chunkToDelta,
67
+ loopEventToChunk,
68
+ partialChunk,
69
+ toCoreOutput,
70
+ type StreamChunk,
71
+ } from "./stream.js";
72
+ import { toAgentTurn, type AgentTurn } from "./request.js";
73
+
74
+ /** Host hooks the kernel supplies at boot (least-privilege; never read from cloc.yml). */
75
+ export interface AgentDeps {
76
+ /** The parsed `agent:` config (config.ts). */
77
+ config: AgentConfig;
78
+ /** By-name handle to the gateway credential (secrets.ts) — resolved lazily, granted at boot. */
79
+ secret: SecretHandle;
80
+ /** Optional OTel span opener the host backs with its real tracer (trace.ts). */
81
+ startSpan?: (name: "agent.generate", context: AgentGenerateContext) => SpanSink;
82
+ /**
83
+ * The §58 policy-before-execution gate every render-time PRIMITIVE access (tool / skill / memory)
84
+ * clears BEFORE execution (027-agentic-primitives FR-014, FR-021; §58). Injected by the kernel
85
+ * from 016-policy-gate; when absent the adapter falls back to {@link ALLOW_ALL_GATE} so the seam
86
+ * is exercisable without 016 present (tests/conformance ONLY — a real deployment injects 016's
87
+ * gate). A denial DEGRADES the render (proceeds without that capability), never bypasses.
88
+ */
89
+ gate?: PolicyGateHook;
90
+ /**
91
+ * OPTIONAL override for how a repair re-prompt is framed from the base prompt + the validation
92
+ * issues. Defaults to {@link defaultRepairPrompt} (the prior, current behavior: the issues are
93
+ * appended as an inert `<validation-issues>` data block, FR-005). A host can swap a richer
94
+ * strategy (e.g. per-kit guidance) WITHOUT touching the adapter; the default preserves behavior.
95
+ */
96
+ frameRepairPrompt?: (args: { basePrompt: string; issues: unknown; attempt: number }) => string;
97
+ }
98
+
99
+ /**
100
+ * The default repair re-prompt framer: append the validation issues as an inert, clearly-delimited
101
+ * DATA block (never as instructions the model could be steered by — FR-015) plus a terse directive
102
+ * to return a corrected object. Extracted so {@link AiSdkAgent.generate} and `stream` share ONE
103
+ * framing and a host can override it via {@link AgentDeps.frameRepairPrompt} (additive seam).
104
+ */
105
+ export function defaultRepairPrompt(args: { basePrompt: string; issues: unknown; attempt: number }): string {
106
+ const { basePrompt, issues, attempt } = args;
107
+ return (
108
+ `${basePrompt}\n\n` +
109
+ `<validation-issues attempt="${attempt}">${JSON.stringify(issues)}</validation-issues>\n` +
110
+ `Return a corrected object that satisfies the schema.`
111
+ );
112
+ }
113
+
114
+ /** The default kit version stamped on the Output meta when the runtime does not pin one. */
115
+ const UNKNOWN_KIT_VERSION = "unknown";
116
+
117
+ /**
118
+ * The assembled `generateText`/`streamText` option base for a turn (vendor types stay internal).
119
+ * `prompt` is always present; the rest of the v6 call surface (output/tools/stopWhen/seed/…) rides
120
+ * along as an opaque record so it can be spread verbatim into a repair re-run. Named so the shared
121
+ * {@link AiSdkAgent}.#repair channel can take it without re-deriving the inline shape.
122
+ */
123
+ type CallOptions = { readonly prompt: string } & Record<string, unknown>;
124
+
125
+ /**
126
+ * The reference Agent. EXACTLY ONE AgentProvider wins per environment, resolved by token (FR-010).
127
+ * Constructed once at boot from the kernel-supplied {@link AgentDeps}; the request path never
128
+ * re-decides the Agent (§75.3).
129
+ */
130
+ export class AiSdkAgent implements AgentProvider {
131
+ #gatewayKey: string | undefined;
132
+ #keyResolved = false;
133
+
134
+ constructor(private readonly deps: AgentDeps) {}
135
+
136
+ /** The §58 gate every primitive access clears; defaults to ALLOW_ALL only when none is wired. */
137
+ get #gate(): PolicyGateHook {
138
+ return this.deps.gate ?? ALLOW_ALL_GATE;
139
+ }
140
+
141
+ /** Resolve the gateway credential BY NAME, once, lazily (FR-009). */
142
+ async #gateway(): Promise<ResolvedGateway> {
143
+ if (!this.#keyResolved) {
144
+ this.#gatewayKey = await resolveGatewayKey(this.deps.secret);
145
+ this.#keyResolved = true;
146
+ }
147
+ return resolveGateway(this.deps.config, this.#gatewayKey);
148
+ }
149
+
150
+ #span(turn: AgentTurn): SpanSink {
151
+ const ctx: AgentGenerateContext = {
152
+ "cloc.data_version": String(turn.providerOptions?.["dataVersion"] ?? ""),
153
+ "cloc.kit_version": String(turn.providerOptions?.["kitVersion"] ?? UNKNOWN_KIT_VERSION),
154
+ "cloc.tier": turn.tier,
155
+ ...(turn.seed !== undefined ? { "cloc.seed": String(turn.seed) } : {}),
156
+ };
157
+ return startAgentSpan(ctx, this.deps.startSpan);
158
+ }
159
+
160
+ /**
161
+ * The shared `generateText`/`streamText` option base for a turn (vendor types stay internal).
162
+ *
163
+ * Assembles the budgeted render-time tool loop (027-agentic-primitives §16b.3): the loop's tool
164
+ * set is the union of (1) the legacy per-turn `tools`, (2) the 027 agentic `GenOpts.tools` from
165
+ * all three sources (`plugin`/`capability`/`wired`, FR-012), and (3) the memory-tool interface
166
+ * when memory is enabled (§16b.2) — each GATED by the §58 gate before execution (FR-014). The
167
+ * budget is `stopWhen` mapped from the core trajectory budget (default `stepCountIs(N)`, §9.1,
168
+ * FR-013); `prepareStep` does per-step context engineering (model swap / activeTools / toolChoice,
169
+ * FR-011). Skill metadata (level-1 only) is prepended to the prompt; the body/bundled load lazily.
170
+ * When NONE of the primitives are set the loop is the plain pre-027 path (FR-002, zero added cost).
171
+ */
172
+ #callOptions(turn: AgentTurn, onEvent: (e: LoopEvent) => void, gw: ResolvedGateway): CallOptions {
173
+ const gate = this.#gate;
174
+ // (1) legacy per-turn tools (gate-checked) + (2) agentic GenOpts.tools (3 sources) + (3) memory.
175
+ const tools: AiToolSet = {
176
+ ...buildToolSet(turn.tools, onEvent, gate),
177
+ ...buildAgenticToolSet(turn.agenticTools, onEvent, gate),
178
+ ...buildMemoryTools(turn.memory, gate, onEvent),
179
+ };
180
+ const prepareStep = toAiPrepareStep(turn.prepareStep, (d) => this.#resolveStepModel(d, gw));
181
+ return {
182
+ output: outputSpecFor(turn.outputSchema),
183
+ tools,
184
+ // The trajectory budget bounds the loop so it can never thrash unbounded (§9.1, FR-013).
185
+ stopWhen: toAiStopWhen(turn.stopWhen),
186
+ ...(prepareStep !== undefined ? { prepareStep } : {}),
187
+ prompt: this.#composePrompt(turn),
188
+ ...(turn.maxTokens !== undefined ? { maxOutputTokens: turn.maxTokens } : {}),
189
+ ...(turn.seed !== undefined ? { seed: turn.seed } : {}),
190
+ ...(turn.signal !== undefined ? { abortSignal: turn.signal } : {}),
191
+ ...(turn.providerOptions !== undefined ? { providerOptions: turn.providerOptions as never } : {}),
192
+ } as const;
193
+ }
194
+
195
+ /** Resolve a `prepareStep` per-step model swap (a core `ModelRef`) to the routed v6 model. */
196
+ #resolveStepModel(directive: StepDirective, gw: ResolvedGateway): PrepareStepResultModel | undefined {
197
+ if (directive.model === undefined) return undefined;
198
+ const id = modelRefToString(directive.model);
199
+ return id ? gw.model(id) : undefined;
200
+ }
201
+
202
+ /**
203
+ * Frame the intent + grounded facts + level-1 skill metadata as a prompt where grounding is inert
204
+ * DATA (FR-015) and skills disclose only `name`+`description` (§16b.1 — the body is NOT in the
205
+ * prompt until activation; FR-005). When no skills/grounding are present this is just the intent.
206
+ */
207
+ #composePrompt(turn: AgentTurn): string {
208
+ const intent = typeof turn.intent === "string" ? turn.intent : JSON.stringify(turn.intent);
209
+ const data = frameGroundingAsData(turn.grounding);
210
+ const skills = turn.skills ? frameSkillsForPrompt(turn.skills) : "";
211
+ return [intent, skills, data].filter((s) => s.length > 0).join("\n\n");
212
+ }
213
+
214
+ // --- AgentProvider.generate ----------------------------------------------
215
+
216
+ async generate(p: Prompt, o?: GenOpts): Promise<Output> {
217
+ const turn = toAgentTurn(p, o, false);
218
+ const span = this.#span(turn);
219
+ const events: LoopEvent[] = [];
220
+ const onEvent = (e: LoopEvent): void => {
221
+ events.push(e);
222
+ span.addEvent(e.kind === "tool-call" ? { name: "tool.call", tool: e.tool } : { name: "tool.result", tool: e.tool });
223
+ };
224
+
225
+ try {
226
+ const gw = await this.#gateway();
227
+ const base = this.#callOptions(turn, onEvent, gw);
228
+
229
+ // Route hosted-first with per-request failover (FR-006/8). Each attempt runs the tool loop
230
+ // and the Output.object path; the first provider that yields a parseable object wins. An
231
+ // abort short-circuits the chain (it is a cancel, not a provider failure; conformance C1).
232
+ const { value: raw, modelId, hop } = await withFailover(
233
+ gw,
234
+ async (model) => generateText({ model, ...base }),
235
+ undefined,
236
+ turn.signal !== undefined ? { signal: turn.signal } : undefined,
237
+ );
238
+
239
+ // Validate-or-repair against the kit schema; re-prompt the SAME provider on repair (FR-005).
240
+ const plan = await validateOrRepair({
241
+ schema: turn.outputSchema,
242
+ initial: readOutput(raw),
243
+ policy: this.deps.config.repair,
244
+ events: {
245
+ onValidate: (ok) => span.addEvent({ name: "validate", ok }),
246
+ onRepair: (attempt, ok) => span.addEvent({ name: "repair", attempt, ok }),
247
+ },
248
+ repair: (issues, attempt) => this.#repair(base, gw, modelId, issues, attempt),
249
+ });
250
+
251
+ const structured = this.#finish(turn, plan, span, { modelId, hop, usage: readUsage(raw) });
252
+ return toCoreOutput(structured, turn.outputSchema as never);
253
+ } catch (err) {
254
+ this.#fail(span, err);
255
+ throw err;
256
+ } finally {
257
+ span.end();
258
+ }
259
+ }
260
+
261
+ // --- AgentProvider.stream ------------------------------------------------
262
+
263
+ stream(p: Prompt, o?: GenOpts): AsyncIterable<Delta> {
264
+ const turn = toAgentTurn(p, o, true);
265
+ const self = this;
266
+ return {
267
+ async *[Symbol.asyncIterator](): AsyncGenerator<Delta> {
268
+ const span = self.#span(turn);
269
+ const queue: StreamChunk[] = [];
270
+ const onEvent = (e: LoopEvent): void => {
271
+ queue.push(loopEventToChunk(e));
272
+ span.addEvent(e.kind === "tool-call" ? { name: "tool.call", tool: e.tool } : { name: "tool.result", tool: e.tool });
273
+ };
274
+ try {
275
+ const gw = await self.#gateway();
276
+ const base = self.#callOptions(turn, onEvent, gw);
277
+
278
+ // Failover for the streaming path: bind the first provider that starts a stream. An
279
+ // abort short-circuits the chain (cancel, not a provider failure; conformance C1).
280
+ const { value: result, modelId, hop } = await withFailover(
281
+ gw,
282
+ async (model) => streamText({ model, ...base }),
283
+ undefined,
284
+ turn.signal !== undefined ? { signal: turn.signal } : undefined,
285
+ );
286
+
287
+ // Drain any tool events captured before the first partial, then stream partial objects.
288
+ for (const c of queue.splice(0)) yield* emit(c);
289
+ for await (const snap of partialOutputStream(result)) {
290
+ for (const c of queue.splice(0)) yield* emit(c);
291
+ yield* emit(partialChunk(snap));
292
+ }
293
+ for (const c of queue.splice(0)) yield* emit(c);
294
+
295
+ // Terminal: validate-or-repair the completed object, then emit the single `final`.
296
+ const finalObject = await readFinalOutput(result);
297
+ const plan = await validateOrRepair({
298
+ schema: turn.outputSchema,
299
+ initial: finalObject,
300
+ policy: self.deps.config.repair,
301
+ events: {
302
+ onValidate: (ok) => span.addEvent({ name: "validate", ok }),
303
+ onRepair: (attempt, ok) => span.addEvent({ name: "repair", attempt, ok }),
304
+ },
305
+ // Streaming repair re-runs non-streamed against the same provider (bounded).
306
+ repair: (issues, attempt) => self.#repair(base, gw, modelId, issues, attempt),
307
+ });
308
+
309
+ const structured = self.#finish(turn, plan, span, { modelId, hop, usage: await readStreamUsage(result) });
310
+ const delta = chunkToDelta<unknown>({ kind: "final", output: structured }, turn.outputSchema as never);
311
+ if (delta) yield delta;
312
+ } catch (err) {
313
+ // Invalidate any streamed partial: surface the error by REJECTING — never a fake `final`
314
+ // the runtime could mistake for completion (edge case, core Delta contract).
315
+ self.#fail(span, err);
316
+ throw err;
317
+ } finally {
318
+ span.end();
319
+ }
320
+
321
+ function* emit(chunk: StreamChunk): Generator<Delta> {
322
+ const d = chunkToDelta<unknown>(chunk, turn.outputSchema as never);
323
+ if (d) yield d;
324
+ }
325
+ },
326
+ };
327
+ }
328
+
329
+ // --- shared repair / finish / fail ---------------------------------------
330
+
331
+ /**
332
+ * The shared validate-or-repair repair channel: re-prompt the SAME provider (`modelId`) with the
333
+ * validation issues framed as an inert data block, NON-streamed, honoring the turn's abort signal.
334
+ * Used by BOTH `generate()` and `stream()` so the two paths repair identically (DRY; FR-005). The
335
+ * framing is overridable via {@link AgentDeps.frameRepairPrompt}; the default reproduces the prior
336
+ * inline behavior exactly.
337
+ */
338
+ async #repair(
339
+ base: CallOptions,
340
+ gw: ResolvedGateway,
341
+ modelId: string,
342
+ issues: unknown,
343
+ attempt: number,
344
+ ): Promise<unknown> {
345
+ const frame = this.deps.frameRepairPrompt ?? defaultRepairPrompt;
346
+ const repaired = await generateText({
347
+ model: gw.model(modelId),
348
+ ...base,
349
+ prompt: frame({ basePrompt: base.prompt, issues, attempt }),
350
+ });
351
+ return readOutput(repaired);
352
+ }
353
+
354
+ #finish<TPlan>(
355
+ turn: AgentTurn,
356
+ plan: TPlan,
357
+ span: SpanSink,
358
+ routed: { modelId: string; hop: number; usage?: ReadUsage },
359
+ ): StructuredOutput<TPlan> {
360
+ const [provider, ...rest] = routed.modelId.split("/");
361
+ const usage = routed.usage;
362
+ span.setAttributes({
363
+ "gateway.provider": provider ?? routed.modelId,
364
+ "gateway.model": rest.join("/") || routed.modelId,
365
+ "gateway.fallback": routed.hop,
366
+ "cost.usd": usage?.costUsd ?? 0,
367
+ ...(usage?.inputTokens !== undefined ? { "prompt.tokens": usage.inputTokens } : {}),
368
+ ...(usage?.outputTokens !== undefined ? { "output.tokens": usage.outputTokens } : {}),
369
+ ...(usage?.totalTokens !== undefined ? { "total.tokens": usage.totalTokens } : {}),
370
+ ...(usage?.cachedInputTokens !== undefined ? { "cache.read.tokens": usage.cachedInputTokens } : {}),
371
+ });
372
+ return makeStructuredOutput({
373
+ plan,
374
+ provenance: collectProvenance(turn.grounding),
375
+ kitVersion: String(turn.providerOptions?.["kitVersion"] ?? UNKNOWN_KIT_VERSION),
376
+ ...(turn.seed !== undefined ? { seed: String(turn.seed) } : {}),
377
+ });
378
+ }
379
+
380
+ #fail(span: SpanSink, err: unknown): void {
381
+ const code = err instanceof AgentError ? err.code : "tool-failed";
382
+ const message = err instanceof Error ? err.message : String(err);
383
+ span.recordError(code, message);
384
+ }
385
+ }
386
+
387
+ // --- AI SDK v6 result readers (localized; the only place we touch vendor result shapes) -------
388
+
389
+ /** The token/cost usage the adapter surfaces to the trace. All fields OPTIONAL (recorded when present). */
390
+ interface ReadUsage {
391
+ inputTokens?: number;
392
+ outputTokens?: number;
393
+ /** Total tokens for the turn (v6 `usage.totalTokens`); recorded additively when reported. */
394
+ totalTokens?: number;
395
+ /** Cached-prompt-token reads (v6 `usage.inputTokenDetails.cacheReadTokens`); when reported. */
396
+ cachedInputTokens?: number;
397
+ /** Per-request cost from the gateway provider metadata (`providerMetadata.gateway.cost`). */
398
+ costUsd?: number;
399
+ }
400
+
401
+ /** The vendor result fields the adapter reads, prefer-stable-then-deprecated. Localized to this file. */
402
+ interface VendorUsage {
403
+ inputTokens?: number;
404
+ outputTokens?: number;
405
+ totalTokens?: number;
406
+ inputTokenDetails?: { cacheReadTokens?: number };
407
+ }
408
+
409
+ /**
410
+ * Read the typed object from a non-streaming v6 result. v6 exposes the stable `result.output`; the
411
+ * older `result.experimental_output` is `@deprecated`. We read the STABLE name first and fall back
412
+ * to the deprecated alias (and `undefined`-safe) so a rename in either direction can't crash us.
413
+ */
414
+ function readOutput(result: unknown): unknown {
415
+ const r = result as { output?: unknown; experimental_output?: unknown };
416
+ return r.output ?? r.experimental_output;
417
+ }
418
+
419
+ /** Map a vendor `LanguageModelUsage` + provider metadata to the adapter's {@link ReadUsage}. */
420
+ function mapUsage(usage: VendorUsage | undefined, cost: number | undefined): ReadUsage | undefined {
421
+ if (!usage && cost === undefined) return undefined;
422
+ return {
423
+ ...(usage?.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
424
+ ...(usage?.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
425
+ ...(usage?.totalTokens !== undefined ? { totalTokens: usage.totalTokens } : {}),
426
+ ...(usage?.inputTokenDetails?.cacheReadTokens !== undefined
427
+ ? { cachedInputTokens: usage.inputTokenDetails.cacheReadTokens }
428
+ : {}),
429
+ ...(cost !== undefined ? { costUsd: cost } : {}),
430
+ };
431
+ }
432
+
433
+ /** Read token/cost usage from a non-streaming v6 result, when present (TODO(C3): mandated-metric set). */
434
+ function readUsage(result: unknown): ReadUsage | undefined {
435
+ const r = result as { usage?: VendorUsage; providerMetadata?: { gateway?: { cost?: number } } };
436
+ return mapUsage(r.usage, r.providerMetadata?.gateway?.cost);
437
+ }
438
+
439
+ /**
440
+ * The v6 partial-object stream: the stable `result.partialOutputStream` (the `experimental_*` alias
441
+ * is `@deprecated`). Each snapshot is a partial of the kit plan — i.e. an object — so we read it as a
442
+ * partial record (assignable to `Partial<unknown>` for {@link partialChunk}; a bare `unknown` is not).
443
+ * We prefer the stable name and fall back to the deprecated alias, then an empty stream.
444
+ */
445
+ function partialOutputStream(result: unknown): AsyncIterable<Record<string, unknown>> {
446
+ const r = result as {
447
+ partialOutputStream?: AsyncIterable<Record<string, unknown>>;
448
+ experimental_partialOutputStream?: AsyncIterable<Record<string, unknown>>;
449
+ };
450
+ return r.partialOutputStream ?? r.experimental_partialOutputStream ?? EMPTY_PARTIAL_STREAM();
451
+ }
452
+
453
+ /** A reusable empty async-iterable for the "no partial stream present" fallback (allocation-light). */
454
+ async function* EMPTY_PARTIAL_STREAM(): AsyncGenerator<Record<string, unknown>> {
455
+ /* yields nothing */
456
+ }
457
+
458
+ /**
459
+ * Await the final typed object of a v6 stream result. v6 exposes the stable `result.output` (a
460
+ * `Promise`); the older `result.experimental_output` is `@deprecated`. We prefer the stable name,
461
+ * fall back to the deprecated alias, and await whichever is a thenable.
462
+ */
463
+ async function readFinalOutput(result: unknown): Promise<unknown> {
464
+ const r = result as { output?: Promise<unknown> | unknown; experimental_output?: Promise<unknown> | unknown };
465
+ const value = r.output ?? r.experimental_output;
466
+ return isThenable(value) ? await value : value;
467
+ }
468
+
469
+ /** Await stream usage (v6 `result.usage` is a Promise on the streaming result). */
470
+ async function readStreamUsage(result: unknown): Promise<ReadUsage | undefined> {
471
+ const r = result as {
472
+ usage?: Promise<VendorUsage> | VendorUsage;
473
+ providerMetadata?: Promise<{ gateway?: { cost?: number } } | undefined> | { gateway?: { cost?: number } };
474
+ };
475
+ const usage = isThenable(r.usage) ? await r.usage : r.usage;
476
+ const meta = isThenable(r.providerMetadata) ? await r.providerMetadata : r.providerMetadata;
477
+ return mapUsage(usage, meta?.gateway?.cost);
478
+ }
479
+
480
+ /** True for a thenable (a Promise or Promise-like), narrowing for `await`. */
481
+ function isThenable<T>(value: unknown): value is PromiseLike<T> {
482
+ return (
483
+ typeof value === "object" &&
484
+ value !== null &&
485
+ typeof (value as { then?: unknown }).then === "function"
486
+ );
487
+ }