@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.
- package/LICENSE +21 -0
- package/dist/agent.d.ts +93 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +359 -0
- package/dist/agent.js.map +1 -0
- package/dist/config.d.ts +85 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +101 -0
- package/dist/config.js.map +1 -0
- package/dist/gateway.d.ts +74 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +96 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-tool.d.ts +63 -0
- package/dist/memory-tool.d.ts.map +1 -0
- package/dist/memory-tool.js +183 -0
- package/dist/memory-tool.js.map +1 -0
- package/dist/output.d.ts +49 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +41 -0
- package/dist/output.js.map +1 -0
- package/dist/plugin.d.ts +74 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +86 -0
- package/dist/plugin.js.map +1 -0
- package/dist/request.d.ts +82 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +80 -0
- package/dist/request.js.map +1 -0
- package/dist/safety.d.ts +54 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +0 -0
- package/dist/safety.js.map +1 -0
- package/dist/secrets.d.ts +51 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +47 -0
- package/dist/secrets.js.map +1 -0
- package/dist/skills-loader.d.ts +76 -0
- package/dist/skills-loader.d.ts.map +1 -0
- package/dist/skills-loader.js +99 -0
- package/dist/skills-loader.js.map +1 -0
- package/dist/stream.d.ts +58 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +59 -0
- package/dist/stream.js.map +1 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +17 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tool-loop.d.ts +98 -0
- package/dist/tool-loop.d.ts.map +1 -0
- package/dist/tool-loop.js +210 -0
- package/dist/tool-loop.js.map +1 -0
- package/dist/trace.d.ts +78 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +39 -0
- package/dist/trace.js.map +1 -0
- package/dist/validate.d.ts +54 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +81 -0
- package/dist/validate.js.map +1 -0
- package/package.json +55 -0
- package/src/agent.ts +487 -0
- package/src/config.ts +147 -0
- package/src/gateway.ts +126 -0
- package/src/index.ts +101 -0
- package/src/memory-tool.ts +219 -0
- package/src/output.ts +67 -0
- package/src/plugin.ts +123 -0
- package/src/request.ts +178 -0
- package/src/safety.ts +0 -0
- package/src/secrets.ts +71 -0
- package/src/skills-loader.ts +153 -0
- package/src/stream.ts +80 -0
- package/src/tokens.ts +82 -0
- package/src/tool-loop.ts +268 -0
- package/src/trace.ts +87 -0
- 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
|
+
}
|