@funkai/agents 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/.generated/req.txt +1 -0
- package/.turbo/turbo-build.log +21 -0
- package/.turbo/turbo-test$colon$coverage.log +109 -0
- package/.turbo/turbo-test.log +141 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +16 -0
- package/ISSUES.md +540 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/banner.svg +97 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/core/agents/base/agent.ts.html +1705 -0
- package/coverage/lcov-report/core/agents/base/index.html +146 -0
- package/coverage/lcov-report/core/agents/base/output.ts.html +256 -0
- package/coverage/lcov-report/core/agents/base/utils.ts.html +694 -0
- package/coverage/lcov-report/core/agents/flow/engine.ts.html +928 -0
- package/coverage/lcov-report/core/agents/flow/flow-agent.ts.html +1462 -0
- package/coverage/lcov-report/core/agents/flow/index.html +146 -0
- package/coverage/lcov-report/core/agents/flow/messages.ts.html +508 -0
- package/coverage/lcov-report/core/agents/flow/steps/factory.ts.html +1975 -0
- package/coverage/lcov-report/core/agents/flow/steps/index.html +116 -0
- package/coverage/lcov-report/core/index.html +131 -0
- package/coverage/lcov-report/core/logger.ts.html +541 -0
- package/coverage/lcov-report/core/models/providers/index.html +116 -0
- package/coverage/lcov-report/core/models/providers/openai.ts.html +337 -0
- package/coverage/lcov-report/core/provider/index.html +131 -0
- package/coverage/lcov-report/core/provider/provider.ts.html +346 -0
- package/coverage/lcov-report/core/provider/usage.ts.html +376 -0
- package/coverage/lcov-report/core/tool.ts.html +577 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +221 -0
- package/coverage/lcov-report/lib/hooks.ts.html +262 -0
- package/coverage/lcov-report/lib/index.html +161 -0
- package/coverage/lcov-report/lib/middleware.ts.html +274 -0
- package/coverage/lcov-report/lib/runnable.ts.html +151 -0
- package/coverage/lcov-report/lib/trace.ts.html +520 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/utils/attempt.ts.html +199 -0
- package/coverage/lcov-report/utils/error.ts.html +421 -0
- package/coverage/lcov-report/utils/index.html +176 -0
- package/coverage/lcov-report/utils/resolve.ts.html +208 -0
- package/coverage/lcov-report/utils/result.ts.html +538 -0
- package/coverage/lcov-report/utils/zod.ts.html +178 -0
- package/coverage/lcov.info +1566 -0
- package/dist/index.d.mts +2883 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2312 -0
- package/dist/index.mjs.map +1 -0
- package/docs/core/agent.md +231 -0
- package/docs/core/hooks.md +95 -0
- package/docs/core/overview.md +87 -0
- package/docs/core/step.md +279 -0
- package/docs/core/tools.md +98 -0
- package/docs/core/workflow.md +235 -0
- package/docs/guides/create-agent.md +224 -0
- package/docs/guides/create-tool.md +137 -0
- package/docs/guides/create-workflow.md +374 -0
- package/docs/overview.md +244 -0
- package/docs/provider/models.md +55 -0
- package/docs/provider/overview.md +106 -0
- package/docs/provider/usage.md +100 -0
- package/docs/research/experimental-context.md +167 -0
- package/docs/research/gap-analysis.md +86 -0
- package/docs/research/prepare-step-and-active-tools.md +138 -0
- package/docs/research/sub-agent-model.md +249 -0
- package/docs/troubleshooting.md +60 -0
- package/logo.svg +17 -0
- package/models.config.json +18 -0
- package/package.json +60 -0
- package/scripts/generate-models.ts +324 -0
- package/src/core/agents/base/agent.test.ts +1522 -0
- package/src/core/agents/base/agent.ts +547 -0
- package/src/core/agents/base/output.test.ts +93 -0
- package/src/core/agents/base/output.ts +57 -0
- package/src/core/agents/base/types.test-d.ts +69 -0
- package/src/core/agents/base/types.ts +503 -0
- package/src/core/agents/base/utils.test.ts +397 -0
- package/src/core/agents/base/utils.ts +197 -0
- package/src/core/agents/flow/engine.test.ts +452 -0
- package/src/core/agents/flow/engine.ts +281 -0
- package/src/core/agents/flow/flow-agent.test.ts +1027 -0
- package/src/core/agents/flow/flow-agent.ts +473 -0
- package/src/core/agents/flow/messages.test.ts +198 -0
- package/src/core/agents/flow/messages.ts +141 -0
- package/src/core/agents/flow/steps/agent.test.ts +280 -0
- package/src/core/agents/flow/steps/agent.ts +87 -0
- package/src/core/agents/flow/steps/all.test.ts +300 -0
- package/src/core/agents/flow/steps/all.ts +73 -0
- package/src/core/agents/flow/steps/builder.ts +124 -0
- package/src/core/agents/flow/steps/each.test.ts +257 -0
- package/src/core/agents/flow/steps/each.ts +61 -0
- package/src/core/agents/flow/steps/factory.test-d.ts +50 -0
- package/src/core/agents/flow/steps/factory.test.ts +1025 -0
- package/src/core/agents/flow/steps/factory.ts +645 -0
- package/src/core/agents/flow/steps/map.test.ts +273 -0
- package/src/core/agents/flow/steps/map.ts +75 -0
- package/src/core/agents/flow/steps/race.test.ts +290 -0
- package/src/core/agents/flow/steps/race.ts +59 -0
- package/src/core/agents/flow/steps/reduce.test.ts +310 -0
- package/src/core/agents/flow/steps/reduce.ts +73 -0
- package/src/core/agents/flow/steps/result.ts +27 -0
- package/src/core/agents/flow/steps/step.test.ts +402 -0
- package/src/core/agents/flow/steps/step.ts +51 -0
- package/src/core/agents/flow/steps/while.test.ts +283 -0
- package/src/core/agents/flow/steps/while.ts +75 -0
- package/src/core/agents/flow/types.ts +348 -0
- package/src/core/logger.test.ts +163 -0
- package/src/core/logger.ts +152 -0
- package/src/core/models/index.test.ts +137 -0
- package/src/core/models/index.ts +152 -0
- package/src/core/models/providers/openai.ts +84 -0
- package/src/core/provider/provider.test.ts +128 -0
- package/src/core/provider/provider.ts +99 -0
- package/src/core/provider/types.ts +98 -0
- package/src/core/provider/usage.test.ts +304 -0
- package/src/core/provider/usage.ts +97 -0
- package/src/core/tool.test.ts +65 -0
- package/src/core/tool.ts +164 -0
- package/src/core/types.ts +66 -0
- package/src/index.ts +95 -0
- package/src/lib/context.test.ts +86 -0
- package/src/lib/context.ts +49 -0
- package/src/lib/hooks.test.ts +102 -0
- package/src/lib/hooks.ts +59 -0
- package/src/lib/middleware.test.ts +122 -0
- package/src/lib/middleware.ts +63 -0
- package/src/lib/runnable.test.ts +41 -0
- package/src/lib/runnable.ts +22 -0
- package/src/lib/trace.test.ts +291 -0
- package/src/lib/trace.ts +145 -0
- package/src/models/index.ts +123 -0
- package/src/models/providers/index.ts +15 -0
- package/src/models/providers/openai.ts +84 -0
- package/src/testing/context.ts +32 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/logger.ts +19 -0
- package/src/utils/attempt.test.ts +127 -0
- package/src/utils/attempt.ts +38 -0
- package/src/utils/error.test.ts +179 -0
- package/src/utils/error.ts +112 -0
- package/src/utils/resolve.test.ts +38 -0
- package/src/utils/resolve.ts +41 -0
- package/src/utils/result.test.ts +79 -0
- package/src/utils/result.ts +151 -0
- package/src/utils/zod.test.ts +69 -0
- package/src/utils/zod.ts +31 -0
- package/tsconfig.json +25 -0
- package/tsdown.config.ts +15 -0
- package/vitest.config.ts +46 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2312 @@
|
|
|
1
|
+
import { Output, generateText, stepCountIs, streamText, tool as tool$1, wrapLanguageModel, zodSchema } from "ai";
|
|
2
|
+
import { attempt, groupBy, isError, isFunction, isMap, isNil, isPrimitive, isSet, isString, sumBy } from "es-toolkit";
|
|
3
|
+
import { has, isObject } from "es-toolkit/compat";
|
|
4
|
+
import { P, match } from "ts-pattern";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createOpenRouter as createOpenRouter$1 } from "@openrouter/ai-sdk-provider";
|
|
7
|
+
//#region src/core/tool.ts
|
|
8
|
+
/**
|
|
9
|
+
* Create a tool for AI agent function calling.
|
|
10
|
+
*
|
|
11
|
+
* Wraps the AI SDK's `tool()` helper. The `inputSchema` (and optional
|
|
12
|
+
* `outputSchema`) are wrapped via `zodSchema()` so the AI SDK can
|
|
13
|
+
* convert them to JSON Schema and validate model I/O at runtime.
|
|
14
|
+
*
|
|
15
|
+
* @see https://ai-sdk.dev/docs/reference/ai-sdk-core/tool
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const fetchPage = tool({
|
|
20
|
+
* description: 'Fetch the contents of a web page by URL',
|
|
21
|
+
* inputSchema: z.object({
|
|
22
|
+
* url: z.url(),
|
|
23
|
+
* }),
|
|
24
|
+
* execute: async ({ url }) => {
|
|
25
|
+
* const res = await fetch(url)
|
|
26
|
+
* return {
|
|
27
|
+
* url,
|
|
28
|
+
* status: res.status,
|
|
29
|
+
* body: await res.text(),
|
|
30
|
+
* }
|
|
31
|
+
* },
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
function tool(config) {
|
|
36
|
+
const resolvedOutputSchema = resolveOutputSchema(config.outputSchema);
|
|
37
|
+
const result = {
|
|
38
|
+
description: config.description,
|
|
39
|
+
title: config.title,
|
|
40
|
+
inputSchema: zodSchema(config.inputSchema),
|
|
41
|
+
outputSchema: resolvedOutputSchema,
|
|
42
|
+
inputExamples: config.inputExamples,
|
|
43
|
+
execute: async (data) => config.execute(data)
|
|
44
|
+
};
|
|
45
|
+
assertTool(result);
|
|
46
|
+
return tool$1(result);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve an optional Zod output schema into a zodSchema-wrapped value.
|
|
50
|
+
*
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
function resolveOutputSchema(schema) {
|
|
54
|
+
return match(schema).with(P.nullish, () => void 0).otherwise((value) => zodSchema(value));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Runtime assertion that narrows `value` to `Tool<TInput, TOutput>`.
|
|
58
|
+
*
|
|
59
|
+
* Validates structural shape at runtime — `inputSchema` is present and
|
|
60
|
+
* `execute` is a function. Generic type parameters (`TInput`, `TOutput`)
|
|
61
|
+
* are erased at runtime, so only the structural shape can be verified.
|
|
62
|
+
*
|
|
63
|
+
* This assertion exists because TypeScript cannot evaluate the AI SDK's
|
|
64
|
+
* `NeverOptional<TOutput>` conditional type when `TOutput` is an
|
|
65
|
+
* unresolved generic — a known limitation with higher-order conditional
|
|
66
|
+
* types. Using `asserts` is TypeScript's endorsed narrowing mechanism
|
|
67
|
+
* and provides a runtime safety net if the AI SDK's tool shape changes.
|
|
68
|
+
*
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
/* v8 ignore start -- defensive guard; tool() always constructs a valid object */
|
|
72
|
+
function assertTool(value) {
|
|
73
|
+
if (isNil(value) || !isObject(value)) throw new TypeError("Expected tool to be an object");
|
|
74
|
+
if (!has(value, "inputSchema")) throw new TypeError("Tool is missing required property: inputSchema");
|
|
75
|
+
if (!has(value, "execute") || !isFunction(value.execute)) throw new TypeError("Tool is missing required property: execute");
|
|
76
|
+
}
|
|
77
|
+
/* v8 ignore stop */
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/utils/zod.ts
|
|
80
|
+
/**
|
|
81
|
+
* Convert a Zod schema to a JSON Schema object.
|
|
82
|
+
*/
|
|
83
|
+
function toJsonSchema(schema) {
|
|
84
|
+
return z.toJSONSchema(schema);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if a Zod schema produces a JSON Schema with `type: 'array'`.
|
|
88
|
+
*
|
|
89
|
+
* Uses JSON Schema output rather than `instanceof` to correctly handle
|
|
90
|
+
* wrapped schemas (transforms, refinements, pipes).
|
|
91
|
+
*/
|
|
92
|
+
function isZodArray(schema) {
|
|
93
|
+
return toJsonSchema(schema).type === "array";
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/core/agents/base/output.ts
|
|
97
|
+
/**
|
|
98
|
+
* Resolve an `OutputParam` into an `OutputSpec`.
|
|
99
|
+
*
|
|
100
|
+
* If the value is already an `OutputSpec`, it is returned as-is.
|
|
101
|
+
* If it is a raw Zod schema:
|
|
102
|
+
* - `z.array(...)` → `Output.array({ element: innerSchema })`
|
|
103
|
+
* - Anything else → `Output.object({ schema })`
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
function resolveOutput(output) {
|
|
108
|
+
return match("parseCompleteOutput" in output).with(true, () => output).otherwise(() => {
|
|
109
|
+
const schema = output;
|
|
110
|
+
return match(isZodArray(schema)).with(true, () => {
|
|
111
|
+
const def = schema._zod;
|
|
112
|
+
if (def != null && def.def.element != null) return Output.array({ element: def.def.element });
|
|
113
|
+
throw new Error("Failed to extract element schema from Zod array. Pass Output.array({ element: elementSchema }) explicitly.");
|
|
114
|
+
}).otherwise(() => Output.object({ schema }));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/core/provider/provider.ts
|
|
119
|
+
/**
|
|
120
|
+
* Create an OpenRouter provider instance with automatic API key resolution.
|
|
121
|
+
*
|
|
122
|
+
* Falls back to the `OPENROUTER_API_KEY` environment variable when
|
|
123
|
+
* no explicit `apiKey` is provided in the options.
|
|
124
|
+
*
|
|
125
|
+
* @param options - Provider settings forwarded to `@openrouter/ai-sdk-provider`.
|
|
126
|
+
* @returns A configured {@link OpenRouterProvider} instance.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const openrouter = createOpenRouter({ apiKey: 'sk-...' })
|
|
131
|
+
* const m = openrouter('openai/gpt-5.2-codex')
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
function createOpenRouter(options) {
|
|
135
|
+
const apiKey = resolveApiKey(options);
|
|
136
|
+
return createOpenRouter$1({
|
|
137
|
+
...options,
|
|
138
|
+
apiKey
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function resolveApiKey(options) {
|
|
142
|
+
if (options != null && options.apiKey != null) return options.apiKey;
|
|
143
|
+
return getOpenRouterApiKey();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Create a cached OpenRouter model resolver.
|
|
147
|
+
*
|
|
148
|
+
* The returned function caches the underlying provider and invalidates
|
|
149
|
+
* when the API key changes at runtime.
|
|
150
|
+
*
|
|
151
|
+
* @returns A function that resolves a model ID to a {@link LanguageModel}.
|
|
152
|
+
*
|
|
153
|
+
* @private
|
|
154
|
+
*/
|
|
155
|
+
function createCachedOpenRouter() {
|
|
156
|
+
const cache = {
|
|
157
|
+
provider: void 0,
|
|
158
|
+
apiKey: void 0
|
|
159
|
+
};
|
|
160
|
+
return (modelId) => {
|
|
161
|
+
const apiKey = getOpenRouterApiKey();
|
|
162
|
+
if (!cache.provider || cache.apiKey !== apiKey) {
|
|
163
|
+
cache.provider = createOpenRouter$1({ apiKey });
|
|
164
|
+
cache.apiKey = apiKey;
|
|
165
|
+
}
|
|
166
|
+
return cache.provider(modelId);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Shorthand for creating a single OpenRouter language model.
|
|
171
|
+
*
|
|
172
|
+
* Resolves the API key from the environment and returns a ready-to-use
|
|
173
|
+
* {@link LanguageModel} that can be passed directly to AI SDK functions.
|
|
174
|
+
*
|
|
175
|
+
* The provider instance is cached at module scope and reused across
|
|
176
|
+
* calls. If `OPENROUTER_API_KEY` changes at runtime, the cache is
|
|
177
|
+
* invalidated and a new provider is created.
|
|
178
|
+
*
|
|
179
|
+
* @param modelId - An OpenRouter model identifier (e.g. `"openai/gpt-5.2-codex"`).
|
|
180
|
+
* @returns A configured {@link LanguageModel} instance.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* const m = openrouter('openai/gpt-5.2-codex')
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
const openrouter = createCachedOpenRouter();
|
|
188
|
+
/**
|
|
189
|
+
* Read the OpenRouter API key from the environment.
|
|
190
|
+
*
|
|
191
|
+
* @throws {Error} If `OPENROUTER_API_KEY` is not set.
|
|
192
|
+
*/
|
|
193
|
+
function getOpenRouterApiKey() {
|
|
194
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
195
|
+
if (!apiKey) throw new Error("OPENROUTER_API_KEY environment variable is required. Set it in your .env file or environment.");
|
|
196
|
+
return apiKey;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/lib/runnable.ts
|
|
200
|
+
/**
|
|
201
|
+
* Symbol key for internal runnable metadata.
|
|
202
|
+
*
|
|
203
|
+
* Stored on Agent and Workflow objects to enable composition:
|
|
204
|
+
* `buildAITools()` reads this to wrap a Runnable as a delegatable
|
|
205
|
+
* tool in parent agents.
|
|
206
|
+
*
|
|
207
|
+
* @internal
|
|
208
|
+
*/
|
|
209
|
+
const RUNNABLE_META = Symbol.for("agent-sdk:runnable-meta");
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/core/agents/base/utils.ts
|
|
212
|
+
/**
|
|
213
|
+
* Resolve a display name for a sub-agent tool from its runnable
|
|
214
|
+
* metadata, falling back to the provided name.
|
|
215
|
+
*
|
|
216
|
+
* @param meta - The runnable metadata, or undefined if not available.
|
|
217
|
+
* @param fallback - The fallback name to use if metadata is missing.
|
|
218
|
+
* @returns The resolved tool name.
|
|
219
|
+
*
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
function resolveToolName(meta, fallback) {
|
|
223
|
+
if (meta != null && meta.name != null) return meta.name;
|
|
224
|
+
return fallback;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Resolve a {@link Model} to an AI SDK `LanguageModel`.
|
|
228
|
+
*/
|
|
229
|
+
function resolveModel(ref) {
|
|
230
|
+
if (typeof ref === "string") return openrouter(ref);
|
|
231
|
+
return ref;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Merge `Tool` records and wrap subagent `Runnable` objects into AI SDK
|
|
235
|
+
* tool format for `generateText` / `streamText`.
|
|
236
|
+
*
|
|
237
|
+
* Tools created via `tool()` are already AI SDK tools and are
|
|
238
|
+
* passed through directly. Only subagents need wrapping.
|
|
239
|
+
*
|
|
240
|
+
* Parent tools are automatically forwarded to sub-agents so they
|
|
241
|
+
* can access the same capabilities (e.g. sandbox filesystem tools)
|
|
242
|
+
* without explicit injection at each call site.
|
|
243
|
+
*/
|
|
244
|
+
function buildAITools(tools, agents) {
|
|
245
|
+
const hasTools = tools != null && Object.keys(tools).length > 0;
|
|
246
|
+
const hasAgents = agents != null && Object.keys(agents).length > 0;
|
|
247
|
+
if (!hasTools && !hasAgents) return;
|
|
248
|
+
const agentTools = agents ? Object.fromEntries(Object.entries(agents).map(([name, runnable]) => {
|
|
249
|
+
const meta = runnable[RUNNABLE_META];
|
|
250
|
+
const toolName = resolveToolName(meta, name);
|
|
251
|
+
return [`agent:${name}`, meta != null && meta.inputSchema != null ? tool$1({
|
|
252
|
+
description: `Delegate to ${toolName}`,
|
|
253
|
+
inputSchema: meta.inputSchema,
|
|
254
|
+
execute: async (input, { abortSignal }) => {
|
|
255
|
+
const r = await runnable.generate(input, {
|
|
256
|
+
signal: abortSignal,
|
|
257
|
+
tools
|
|
258
|
+
});
|
|
259
|
+
if (!r.ok) throw new Error(r.error.message);
|
|
260
|
+
return r.output;
|
|
261
|
+
}
|
|
262
|
+
}) : tool$1({
|
|
263
|
+
description: `Delegate to ${toolName}`,
|
|
264
|
+
inputSchema: z.object({ prompt: z.string().describe("The prompt to send") }),
|
|
265
|
+
execute: async (input, { abortSignal }) => {
|
|
266
|
+
const r = await runnable.generate(input.prompt, {
|
|
267
|
+
signal: abortSignal,
|
|
268
|
+
tools
|
|
269
|
+
});
|
|
270
|
+
if (!r.ok) throw new Error(r.error.message);
|
|
271
|
+
return r.output;
|
|
272
|
+
}
|
|
273
|
+
})];
|
|
274
|
+
})) : {};
|
|
275
|
+
return {
|
|
276
|
+
...tools,
|
|
277
|
+
...agentTools
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Resolve the system prompt from config or override.
|
|
282
|
+
*/
|
|
283
|
+
function resolveSystem(system, input) {
|
|
284
|
+
if (system == null) return;
|
|
285
|
+
if (typeof system === "function") return system({ input });
|
|
286
|
+
return system;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Build the prompt/messages from input based on mode (typed vs simple).
|
|
290
|
+
*
|
|
291
|
+
* Returns a discriminated object: either `{ prompt }` or `{ messages }`,
|
|
292
|
+
* never both — matching the AI SDK's `Prompt` union type.
|
|
293
|
+
*/
|
|
294
|
+
function buildPrompt(input, config) {
|
|
295
|
+
return match({
|
|
296
|
+
hasInput: Boolean(config.input),
|
|
297
|
+
hasPrompt: Boolean(config.prompt)
|
|
298
|
+
}).with({
|
|
299
|
+
hasInput: true,
|
|
300
|
+
hasPrompt: false
|
|
301
|
+
}, () => {
|
|
302
|
+
throw new Error("Agent has `input` schema but no `prompt` function — both are required for typed mode");
|
|
303
|
+
}).with({
|
|
304
|
+
hasInput: false,
|
|
305
|
+
hasPrompt: true
|
|
306
|
+
}, () => {
|
|
307
|
+
throw new Error("Agent has `prompt` function but no `input` schema — both are required for typed mode");
|
|
308
|
+
}).with({
|
|
309
|
+
hasInput: true,
|
|
310
|
+
hasPrompt: true
|
|
311
|
+
}, () => {
|
|
312
|
+
const promptFn = config.prompt;
|
|
313
|
+
const built = promptFn({ input });
|
|
314
|
+
return match(typeof built === "string").with(true, () => ({ prompt: built })).otherwise(() => ({ messages: built }));
|
|
315
|
+
}).otherwise(() => match(typeof input === "string").with(true, () => ({ prompt: input })).otherwise(() => ({ messages: input })));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Convert AI SDK's `LanguageModelUsage` to our flat `TokenUsage`.
|
|
319
|
+
*
|
|
320
|
+
* Maps nested `inputTokenDetails` / `outputTokenDetails` to flat
|
|
321
|
+
* fields, resolving `undefined` → `0`.
|
|
322
|
+
*
|
|
323
|
+
* @param usage - The AI SDK usage object (from `totalUsage`).
|
|
324
|
+
* @returns A resolved {@link TokenUsage} with all fields as numbers.
|
|
325
|
+
*/
|
|
326
|
+
function toTokenUsage(usage) {
|
|
327
|
+
const inputDetails = match(usage.inputTokenDetails).with(P.nonNullable, (d) => ({
|
|
328
|
+
cacheReadTokens: d.cacheReadTokens ?? 0,
|
|
329
|
+
cacheWriteTokens: d.cacheWriteTokens ?? 0
|
|
330
|
+
})).otherwise(() => ({
|
|
331
|
+
cacheReadTokens: 0,
|
|
332
|
+
cacheWriteTokens: 0
|
|
333
|
+
}));
|
|
334
|
+
const outputDetails = match(usage.outputTokenDetails).with(P.nonNullable, (d) => ({ reasoningTokens: d.reasoningTokens ?? 0 })).otherwise(() => ({ reasoningTokens: 0 }));
|
|
335
|
+
return {
|
|
336
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
337
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
338
|
+
totalTokens: usage.totalTokens ?? 0,
|
|
339
|
+
cacheReadTokens: inputDetails.cacheReadTokens,
|
|
340
|
+
cacheWriteTokens: inputDetails.cacheWriteTokens,
|
|
341
|
+
reasoningTokens: outputDetails.reasoningTokens
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/core/logger.ts
|
|
346
|
+
/**
|
|
347
|
+
* Create a minimal console-based logger satisfying the {@link Logger} interface.
|
|
348
|
+
*
|
|
349
|
+
* Supports `child()` by merging bindings into a prefix object.
|
|
350
|
+
* Each log call prepends the accumulated bindings to the output.
|
|
351
|
+
*
|
|
352
|
+
* Used as the default when no pino-compatible logger is injected.
|
|
353
|
+
*/
|
|
354
|
+
function createDefaultLogger(bindings) {
|
|
355
|
+
const prefix = bindings ?? {};
|
|
356
|
+
return {
|
|
357
|
+
debug(first, second) {
|
|
358
|
+
writeLog(prefix, "debug", first, second);
|
|
359
|
+
},
|
|
360
|
+
info(first, second) {
|
|
361
|
+
writeLog(prefix, "info", first, second);
|
|
362
|
+
},
|
|
363
|
+
warn(first, second) {
|
|
364
|
+
writeLog(prefix, "warn", first, second);
|
|
365
|
+
},
|
|
366
|
+
error(first, second) {
|
|
367
|
+
writeLog(prefix, "error", first, second);
|
|
368
|
+
},
|
|
369
|
+
child(childBindings) {
|
|
370
|
+
return createDefaultLogger({
|
|
371
|
+
...prefix,
|
|
372
|
+
...childBindings
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
*
|
|
379
|
+
* @param bindings
|
|
380
|
+
* @param level
|
|
381
|
+
* @param first
|
|
382
|
+
* @param second
|
|
383
|
+
* @private
|
|
384
|
+
*/
|
|
385
|
+
function writeLog(bindings, level, first, second) {
|
|
386
|
+
if (typeof first === "string") {
|
|
387
|
+
const meta = second;
|
|
388
|
+
console[level]({
|
|
389
|
+
...bindings,
|
|
390
|
+
...meta
|
|
391
|
+
}, first);
|
|
392
|
+
} else console[level]({
|
|
393
|
+
...bindings,
|
|
394
|
+
...first
|
|
395
|
+
}, second);
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/lib/hooks.ts
|
|
399
|
+
const formatHookError = (err) => match(err).when((e) => e instanceof Error, (e) => e.message).otherwise((e) => String(e));
|
|
400
|
+
/**
|
|
401
|
+
* Wrap a nullable hook into a callback for `fireHooks`, avoiding
|
|
402
|
+
* optional chaining, ternaries, and non-null assertions.
|
|
403
|
+
*
|
|
404
|
+
* @param hookFn - The hook callback, or undefined if not configured.
|
|
405
|
+
* @param event - The event payload to pass to the hook.
|
|
406
|
+
* @returns A thunk that calls the hook with the event, or undefined.
|
|
407
|
+
*
|
|
408
|
+
* @private
|
|
409
|
+
*/
|
|
410
|
+
function wrapHook(hookFn, event) {
|
|
411
|
+
if (hookFn !== void 0) return () => hookFn(event);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Run hook callbacks sequentially, logging errors at warn level.
|
|
415
|
+
*
|
|
416
|
+
* Unlike `attemptEachAsync`, this function surfaces errors via the
|
|
417
|
+
* logger so hook failures are visible in diagnostic output.
|
|
418
|
+
*
|
|
419
|
+
* @param log - Logger for warning about hook errors.
|
|
420
|
+
* @param handlers - Callbacks to execute in order. `undefined` entries are skipped.
|
|
421
|
+
*/
|
|
422
|
+
async function fireHooks(log, ...handlers) {
|
|
423
|
+
for (const h of handlers) if (h != null) try {
|
|
424
|
+
await h();
|
|
425
|
+
} catch (err) {
|
|
426
|
+
const errorMessage = formatHookError(err);
|
|
427
|
+
log.warn("hook error", { error: errorMessage });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/lib/middleware.ts
|
|
432
|
+
/**
|
|
433
|
+
* Wrap a language model with middleware.
|
|
434
|
+
*
|
|
435
|
+
* In development (`NODE_ENV === 'development'`), the AI SDK devtools
|
|
436
|
+
* middleware is appended automatically. Any additional middleware
|
|
437
|
+
* provided in the options is applied first (outermost).
|
|
438
|
+
*
|
|
439
|
+
* @param options - The model and optional middleware configuration.
|
|
440
|
+
* @returns A wrapped language model with middleware applied.
|
|
441
|
+
*/
|
|
442
|
+
async function withModelMiddleware(options) {
|
|
443
|
+
const useDevtools = options.devtools === true || options.devtools !== false && process.env.NODE_ENV === "development";
|
|
444
|
+
const defaultMiddleware = [];
|
|
445
|
+
if (useDevtools) {
|
|
446
|
+
const { devToolsMiddleware } = await import("@ai-sdk/devtools");
|
|
447
|
+
defaultMiddleware.push(devToolsMiddleware());
|
|
448
|
+
}
|
|
449
|
+
const middleware = [];
|
|
450
|
+
if (options.middleware) middleware.push(...options.middleware, ...defaultMiddleware);
|
|
451
|
+
else middleware.push(...defaultMiddleware);
|
|
452
|
+
if (middleware.length === 0) return options.model;
|
|
453
|
+
return wrapLanguageModel({
|
|
454
|
+
model: options.model,
|
|
455
|
+
middleware
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/utils/error.ts
|
|
460
|
+
/**
|
|
461
|
+
* Coerces an unknown thrown value into a proper `Error` instance.
|
|
462
|
+
*
|
|
463
|
+
* Handles the common cases where libraries throw non-`Error` values
|
|
464
|
+
* (e.g. plain API response bodies, arrays, Maps) that would otherwise
|
|
465
|
+
* serialize as `[object Object]` in error messages.
|
|
466
|
+
*
|
|
467
|
+
* @param thrown - The caught value from a `catch` block.
|
|
468
|
+
* @returns An `Error` with a meaningful `.message`. If `thrown` is
|
|
469
|
+
* already an `Error`, it is returned as-is. The original value is
|
|
470
|
+
* preserved as `.cause` for debugging.
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```typescript
|
|
474
|
+
* try {
|
|
475
|
+
* await riskyCall()
|
|
476
|
+
* } catch (thrown) {
|
|
477
|
+
* const error = toError(thrown)
|
|
478
|
+
* console.error(error.message)
|
|
479
|
+
* }
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
function toError(thrown) {
|
|
483
|
+
if (isError(thrown)) return thrown;
|
|
484
|
+
if (isString(thrown)) return new Error(thrown, { cause: thrown });
|
|
485
|
+
return new Error(safeStringify(thrown), { cause: thrown });
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Produce a human-readable string from any unknown value.
|
|
489
|
+
*
|
|
490
|
+
* Uses `JSON.stringify` for structured types (plain objects, arrays)
|
|
491
|
+
* so the message contains actual content instead of `[object Object]`.
|
|
492
|
+
* Maps and Sets are converted to their array representation first.
|
|
493
|
+
* Falls back to `String()` for primitives or when serialization fails
|
|
494
|
+
* (e.g. circular references).
|
|
495
|
+
*
|
|
496
|
+
* @param value - The value to stringify.
|
|
497
|
+
* @returns A meaningful string representation.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* ```typescript
|
|
501
|
+
* safeStringify({ status: 400 }) // '{"status":400}'
|
|
502
|
+
* safeStringify(new Map([['k', 'v']])) // '[["k","v"]]'
|
|
503
|
+
* safeStringify(null) // 'null'
|
|
504
|
+
* safeStringify(42) // '42'
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
function safeStringify(value) {
|
|
508
|
+
if (isNil(value) || isPrimitive(value)) return String(value);
|
|
509
|
+
return safeStringifyJSON(value) || String(value);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Safely serializes a value to a JSON string without throwing.
|
|
513
|
+
*
|
|
514
|
+
* Converts types that `JSON.stringify` handles poorly (Maps, Sets)
|
|
515
|
+
* into serializable equivalents before stringifying. Returns an empty
|
|
516
|
+
* string when serialization fails (e.g. circular references).
|
|
517
|
+
*
|
|
518
|
+
* @param value - The value to serialize.
|
|
519
|
+
* @returns The JSON string, or an empty string if serialization fails.
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* ```typescript
|
|
523
|
+
* safeStringifyJSON({ status: 400 }) // '{"status":400}'
|
|
524
|
+
* safeStringifyJSON(new Map([['k', 'v']])) // '[["k","v"]]'
|
|
525
|
+
* safeStringifyJSON(circularObj) // ''
|
|
526
|
+
* ```
|
|
527
|
+
*/
|
|
528
|
+
function safeStringifyJSON(value) {
|
|
529
|
+
const serializable = toSerializable(value);
|
|
530
|
+
const [error, json] = attempt(() => JSON.stringify(serializable));
|
|
531
|
+
if (!isNil(error) || isNil(json)) return "";
|
|
532
|
+
return json;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Convert types that `JSON.stringify` handles poorly into
|
|
536
|
+
* serializable equivalents.
|
|
537
|
+
*
|
|
538
|
+
* - `Map` → array of `[key, value]` entries
|
|
539
|
+
* - `Set` → array of values
|
|
540
|
+
* - Everything else → passed through unchanged
|
|
541
|
+
*
|
|
542
|
+
* @private
|
|
543
|
+
* @param value - The value to normalize.
|
|
544
|
+
* @returns A JSON-friendly representation.
|
|
545
|
+
*/
|
|
546
|
+
function toSerializable(value) {
|
|
547
|
+
if (isMap(value)) return Array.from(value.entries());
|
|
548
|
+
if (isSet(value)) return Array.from(value);
|
|
549
|
+
return value;
|
|
550
|
+
}
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/core/agents/base/agent.ts
|
|
553
|
+
/**
|
|
554
|
+
* Safely read a property from `overrides`, which may be undefined.
|
|
555
|
+
* Replaces `overrides?.prop` optional chaining.
|
|
556
|
+
*
|
|
557
|
+
* @private
|
|
558
|
+
*/
|
|
559
|
+
function readOverride(overrides, key) {
|
|
560
|
+
if (overrides !== void 0) return overrides[key];
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Safely compute the JSON-serialized length of a value.
|
|
564
|
+
* Returns 0 if serialization fails (e.g. circular refs, BigInt).
|
|
565
|
+
*
|
|
566
|
+
* @private
|
|
567
|
+
*/
|
|
568
|
+
function safeSerializedLength(value) {
|
|
569
|
+
try {
|
|
570
|
+
const json = JSON.stringify(value);
|
|
571
|
+
return typeof json === "string" ? json.length : 0;
|
|
572
|
+
} catch {
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Return the value if the predicate is true, otherwise undefined.
|
|
578
|
+
* Replaces `predicate ? value : undefined` ternary.
|
|
579
|
+
*
|
|
580
|
+
* @private
|
|
581
|
+
*/
|
|
582
|
+
function valueOrUndefined(predicate, value) {
|
|
583
|
+
if (predicate) return value;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Resolve an optional output param. Returns `resolveOutput(param)` if
|
|
587
|
+
* param is defined, otherwise undefined.
|
|
588
|
+
*
|
|
589
|
+
* @private
|
|
590
|
+
*/
|
|
591
|
+
function resolveOptionalOutput(param) {
|
|
592
|
+
if (param !== void 0) return resolveOutput(param);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Safely extract a property from an object, returning `{}` if the
|
|
596
|
+
* property does not exist. Replaces `'key' in obj ? obj[key] : {}` ternary.
|
|
597
|
+
*
|
|
598
|
+
* @private
|
|
599
|
+
*/
|
|
600
|
+
function extractProperty(obj, key) {
|
|
601
|
+
if (key in obj) return obj[key];
|
|
602
|
+
return {};
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Extract token usage from a step's usage object, defaulting to 0
|
|
606
|
+
* when usage is undefined. Replaces optional chaining on `step.usage`.
|
|
607
|
+
*
|
|
608
|
+
* @private
|
|
609
|
+
*/
|
|
610
|
+
function extractUsage(usage) {
|
|
611
|
+
if (usage !== void 0) {
|
|
612
|
+
const inputTokens = usage.inputTokens ?? 0;
|
|
613
|
+
const outputTokens = usage.outputTokens ?? 0;
|
|
614
|
+
return {
|
|
615
|
+
inputTokens,
|
|
616
|
+
outputTokens,
|
|
617
|
+
totalTokens: usage.totalTokens ?? inputTokens + outputTokens
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
inputTokens: 0,
|
|
622
|
+
outputTokens: 0,
|
|
623
|
+
totalTokens: 0
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Return `ifOutput` when `output` is defined, `ifText` otherwise.
|
|
628
|
+
* Replaces `output ? aiResult.output : aiResult.text` ternary.
|
|
629
|
+
*
|
|
630
|
+
* @private
|
|
631
|
+
*/
|
|
632
|
+
function pickByOutput(output, ifOutput, ifText) {
|
|
633
|
+
if (output !== void 0) return ifOutput;
|
|
634
|
+
return ifText;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Create an agent with typed input, tools, subagents, and hooks.
|
|
638
|
+
*
|
|
639
|
+
* Agents run a tool loop (via the AI SDK's `generateText`) until a
|
|
640
|
+
* stop condition is met. They support:
|
|
641
|
+
* - **Typed input** via Zod schema + prompt template.
|
|
642
|
+
* - **Simple mode** — pass a string or messages directly.
|
|
643
|
+
* - **Tools** for function calling.
|
|
644
|
+
* - **Subagents** auto-wrapped as delegatable tools.
|
|
645
|
+
* - **Inline overrides** per call.
|
|
646
|
+
* - **Hooks** for observability.
|
|
647
|
+
* - **Result return type** that never throws.
|
|
648
|
+
*
|
|
649
|
+
* @typeParam TInput - Agent input type (default: `string | Message[]`).
|
|
650
|
+
* @typeParam TOutput - Agent output type (default: `string`).
|
|
651
|
+
* @typeParam TTools - Record of tools.
|
|
652
|
+
* @typeParam TSubAgents - Record of subagents.
|
|
653
|
+
* @param config - Agent configuration including name, model, schemas,
|
|
654
|
+
* tools, subagents, hooks, and logger.
|
|
655
|
+
* @returns An `Agent` instance with `.generate()`, `.stream()`, and `.fn()`.
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* ```typescript
|
|
659
|
+
* // Simple mode — pass a string directly
|
|
660
|
+
* const helper = agent({
|
|
661
|
+
* name: 'helper',
|
|
662
|
+
* model: 'openai/gpt-4.1',
|
|
663
|
+
* system: 'You are a helpful assistant.',
|
|
664
|
+
* })
|
|
665
|
+
* await helper.generate('What is TypeScript?')
|
|
666
|
+
*
|
|
667
|
+
* // Typed mode — input schema + prompt template
|
|
668
|
+
* const summarizer = agent({
|
|
669
|
+
* name: 'summarizer',
|
|
670
|
+
* input: z.object({ text: z.string() }),
|
|
671
|
+
* model: 'openai/gpt-4.1',
|
|
672
|
+
* prompt: ({ input }) => `Summarize:\n\n${input.text}`,
|
|
673
|
+
* })
|
|
674
|
+
* await summarizer.generate({ text: '...' })
|
|
675
|
+
*
|
|
676
|
+
* // Export as a plain function
|
|
677
|
+
* export const summarize = summarizer.fn()
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
function agent(config) {
|
|
681
|
+
const baseLogger = config.logger ?? createDefaultLogger();
|
|
682
|
+
/**
|
|
683
|
+
* Validate raw input against the config schema, if present.
|
|
684
|
+
*
|
|
685
|
+
* Returns a discriminated union: `{ ok: true, input }` on success,
|
|
686
|
+
* `{ ok: false, error }` when validation fails.
|
|
687
|
+
*
|
|
688
|
+
* @private
|
|
689
|
+
*/
|
|
690
|
+
function validateInput(rawInput) {
|
|
691
|
+
if (!config.input) return {
|
|
692
|
+
ok: true,
|
|
693
|
+
input: rawInput
|
|
694
|
+
};
|
|
695
|
+
const parsed = config.input.safeParse(rawInput);
|
|
696
|
+
if (!parsed.success) return {
|
|
697
|
+
ok: false,
|
|
698
|
+
error: {
|
|
699
|
+
code: "VALIDATION_ERROR",
|
|
700
|
+
message: `Input validation failed: ${parsed.error.message}`
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
return {
|
|
704
|
+
ok: true,
|
|
705
|
+
input: parsed.data
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async function generate(rawInput, overrides) {
|
|
709
|
+
const validated = validateInput(rawInput);
|
|
710
|
+
if (!validated.ok) return {
|
|
711
|
+
ok: false,
|
|
712
|
+
error: validated.error
|
|
713
|
+
};
|
|
714
|
+
const input = validated.input;
|
|
715
|
+
const log = (readOverride(overrides, "logger") ?? baseLogger).child({ agentId: config.name });
|
|
716
|
+
const startedAt = Date.now();
|
|
717
|
+
try {
|
|
718
|
+
const model = await withModelMiddleware({ model: resolveModel(readOverride(overrides, "model") ?? config.model) });
|
|
719
|
+
const overrideTools = readOverride(overrides, "tools");
|
|
720
|
+
const overrideAgents = readOverride(overrides, "agents");
|
|
721
|
+
const mergedTools = {
|
|
722
|
+
...config.tools,
|
|
723
|
+
...overrideTools
|
|
724
|
+
};
|
|
725
|
+
const mergedAgents = {
|
|
726
|
+
...config.agents,
|
|
727
|
+
...overrideAgents
|
|
728
|
+
};
|
|
729
|
+
const hasTools = Object.keys(mergedTools).length > 0;
|
|
730
|
+
const hasAgents = Object.keys(mergedAgents).length > 0;
|
|
731
|
+
const aiTools = buildAITools(valueOrUndefined(hasTools, mergedTools), valueOrUndefined(hasAgents, mergedAgents));
|
|
732
|
+
const system = resolveSystem(readOverride(overrides, "system") ?? config.system, input);
|
|
733
|
+
const promptParams = buildPrompt(input, config);
|
|
734
|
+
const output = resolveOptionalOutput(readOverride(overrides, "output") ?? config.output);
|
|
735
|
+
await fireHooks(log, wrapHook(config.onStart, { input }), wrapHook(readOverride(overrides, "onStart"), { input }));
|
|
736
|
+
log.debug("agent.generate start", { name: config.name });
|
|
737
|
+
const maxSteps = readOverride(overrides, "maxSteps") ?? config.maxSteps ?? 20;
|
|
738
|
+
const overrideSignal = readOverride(overrides, "signal");
|
|
739
|
+
const stepCounter = { value: 0 };
|
|
740
|
+
const aiResult = await generateText({
|
|
741
|
+
model,
|
|
742
|
+
system,
|
|
743
|
+
...promptParams,
|
|
744
|
+
tools: aiTools,
|
|
745
|
+
output,
|
|
746
|
+
stopWhen: stepCountIs(maxSteps),
|
|
747
|
+
abortSignal: overrideSignal,
|
|
748
|
+
onStepFinish: async (step) => {
|
|
749
|
+
const event = {
|
|
750
|
+
stepId: `${config.name}:${stepCounter.value++}`,
|
|
751
|
+
toolCalls: (step.toolCalls ?? []).map((tc) => {
|
|
752
|
+
const args = extractProperty(tc, "args");
|
|
753
|
+
return {
|
|
754
|
+
toolName: tc.toolName,
|
|
755
|
+
argsTextLength: safeSerializedLength(args)
|
|
756
|
+
};
|
|
757
|
+
}),
|
|
758
|
+
toolResults: (step.toolResults ?? []).map((tr) => {
|
|
759
|
+
const result = extractProperty(tr, "result");
|
|
760
|
+
return {
|
|
761
|
+
toolName: tr.toolName,
|
|
762
|
+
resultTextLength: safeSerializedLength(result)
|
|
763
|
+
};
|
|
764
|
+
}),
|
|
765
|
+
usage: extractUsage(step.usage)
|
|
766
|
+
};
|
|
767
|
+
await fireHooks(log, wrapHook(config.onStepFinish, event), wrapHook(readOverride(overrides, "onStepFinish"), event));
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
const duration = Date.now() - startedAt;
|
|
771
|
+
const generateResult = {
|
|
772
|
+
output: pickByOutput(output, aiResult.output, aiResult.text),
|
|
773
|
+
messages: aiResult.response.messages,
|
|
774
|
+
usage: toTokenUsage(aiResult.totalUsage),
|
|
775
|
+
finishReason: aiResult.finishReason
|
|
776
|
+
};
|
|
777
|
+
await fireHooks(log, wrapHook(config.onFinish, {
|
|
778
|
+
input,
|
|
779
|
+
result: generateResult,
|
|
780
|
+
duration
|
|
781
|
+
}), wrapHook(readOverride(overrides, "onFinish"), {
|
|
782
|
+
input,
|
|
783
|
+
result: generateResult,
|
|
784
|
+
duration
|
|
785
|
+
}));
|
|
786
|
+
log.debug("agent.generate finish", {
|
|
787
|
+
name: config.name,
|
|
788
|
+
duration
|
|
789
|
+
});
|
|
790
|
+
return {
|
|
791
|
+
ok: true,
|
|
792
|
+
...generateResult
|
|
793
|
+
};
|
|
794
|
+
} catch (thrown) {
|
|
795
|
+
const error = toError(thrown);
|
|
796
|
+
const duration = Date.now() - startedAt;
|
|
797
|
+
log.error("agent.generate error", {
|
|
798
|
+
name: config.name,
|
|
799
|
+
error: error.message,
|
|
800
|
+
duration
|
|
801
|
+
});
|
|
802
|
+
await fireHooks(log, wrapHook(config.onError, {
|
|
803
|
+
input,
|
|
804
|
+
error
|
|
805
|
+
}), wrapHook(readOverride(overrides, "onError"), {
|
|
806
|
+
input,
|
|
807
|
+
error
|
|
808
|
+
}));
|
|
809
|
+
return {
|
|
810
|
+
ok: false,
|
|
811
|
+
error: {
|
|
812
|
+
code: "AGENT_ERROR",
|
|
813
|
+
message: error.message,
|
|
814
|
+
cause: error
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async function stream(rawInput, overrides) {
|
|
820
|
+
const validated = validateInput(rawInput);
|
|
821
|
+
if (!validated.ok) return {
|
|
822
|
+
ok: false,
|
|
823
|
+
error: validated.error
|
|
824
|
+
};
|
|
825
|
+
const input = validated.input;
|
|
826
|
+
const log = (readOverride(overrides, "logger") ?? baseLogger).child({ agentId: config.name });
|
|
827
|
+
const startedAt = Date.now();
|
|
828
|
+
try {
|
|
829
|
+
const model = await withModelMiddleware({ model: resolveModel(readOverride(overrides, "model") ?? config.model) });
|
|
830
|
+
const overrideTools = readOverride(overrides, "tools");
|
|
831
|
+
const overrideAgents = readOverride(overrides, "agents");
|
|
832
|
+
const mergedTools = {
|
|
833
|
+
...config.tools,
|
|
834
|
+
...overrideTools
|
|
835
|
+
};
|
|
836
|
+
const mergedAgents = {
|
|
837
|
+
...config.agents,
|
|
838
|
+
...overrideAgents
|
|
839
|
+
};
|
|
840
|
+
const hasTools = Object.keys(mergedTools).length > 0;
|
|
841
|
+
const hasAgents = Object.keys(mergedAgents).length > 0;
|
|
842
|
+
const aiTools = buildAITools(valueOrUndefined(hasTools, mergedTools), valueOrUndefined(hasAgents, mergedAgents));
|
|
843
|
+
const system = resolveSystem(readOverride(overrides, "system") ?? config.system, input);
|
|
844
|
+
const promptParams = buildPrompt(input, config);
|
|
845
|
+
const output = resolveOptionalOutput(readOverride(overrides, "output") ?? config.output);
|
|
846
|
+
await fireHooks(log, wrapHook(config.onStart, { input }), wrapHook(readOverride(overrides, "onStart"), { input }));
|
|
847
|
+
log.debug("agent.stream start", { name: config.name });
|
|
848
|
+
const maxSteps = readOverride(overrides, "maxSteps") ?? config.maxSteps ?? 20;
|
|
849
|
+
const overrideSignal = readOverride(overrides, "signal");
|
|
850
|
+
const stepCounter = { value: 0 };
|
|
851
|
+
const aiResult = streamText({
|
|
852
|
+
model,
|
|
853
|
+
system,
|
|
854
|
+
...promptParams,
|
|
855
|
+
tools: aiTools,
|
|
856
|
+
output,
|
|
857
|
+
stopWhen: stepCountIs(maxSteps),
|
|
858
|
+
abortSignal: overrideSignal,
|
|
859
|
+
onStepFinish: async (step) => {
|
|
860
|
+
const event = {
|
|
861
|
+
stepId: `${config.name}:${stepCounter.value++}`,
|
|
862
|
+
toolCalls: (step.toolCalls ?? []).map((tc) => {
|
|
863
|
+
const args = extractProperty(tc, "args");
|
|
864
|
+
return {
|
|
865
|
+
toolName: tc.toolName,
|
|
866
|
+
argsTextLength: safeSerializedLength(args)
|
|
867
|
+
};
|
|
868
|
+
}),
|
|
869
|
+
toolResults: (step.toolResults ?? []).map((tr) => {
|
|
870
|
+
const result = extractProperty(tr, "result");
|
|
871
|
+
return {
|
|
872
|
+
toolName: tr.toolName,
|
|
873
|
+
resultTextLength: safeSerializedLength(result)
|
|
874
|
+
};
|
|
875
|
+
}),
|
|
876
|
+
usage: extractUsage(step.usage)
|
|
877
|
+
};
|
|
878
|
+
await fireHooks(log, wrapHook(config.onStepFinish, event), wrapHook(readOverride(overrides, "onStepFinish"), event));
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
const { readable, writable } = new TransformStream();
|
|
882
|
+
const done = (async () => {
|
|
883
|
+
const writer = writable.getWriter();
|
|
884
|
+
try {
|
|
885
|
+
for await (const part of aiResult.fullStream) await writer.write(part);
|
|
886
|
+
await writer.close();
|
|
887
|
+
} catch (error) {
|
|
888
|
+
await writer.abort(error).catch(() => {});
|
|
889
|
+
throw error;
|
|
890
|
+
}
|
|
891
|
+
const finalOutput = pickByOutput(output, await aiResult.output, await aiResult.text);
|
|
892
|
+
const finalMessages = (await aiResult.response).messages;
|
|
893
|
+
const finalUsage = toTokenUsage(await aiResult.totalUsage);
|
|
894
|
+
const finalFinishReason = await aiResult.finishReason;
|
|
895
|
+
const duration = Date.now() - startedAt;
|
|
896
|
+
const generateResult = {
|
|
897
|
+
output: finalOutput,
|
|
898
|
+
messages: finalMessages,
|
|
899
|
+
usage: finalUsage,
|
|
900
|
+
finishReason: finalFinishReason
|
|
901
|
+
};
|
|
902
|
+
await fireHooks(log, wrapHook(config.onFinish, {
|
|
903
|
+
input,
|
|
904
|
+
result: generateResult,
|
|
905
|
+
duration
|
|
906
|
+
}), wrapHook(readOverride(overrides, "onFinish"), {
|
|
907
|
+
input,
|
|
908
|
+
result: generateResult,
|
|
909
|
+
duration
|
|
910
|
+
}));
|
|
911
|
+
log.debug("agent.stream finish", {
|
|
912
|
+
name: config.name,
|
|
913
|
+
duration
|
|
914
|
+
});
|
|
915
|
+
return {
|
|
916
|
+
output: finalOutput,
|
|
917
|
+
messages: finalMessages,
|
|
918
|
+
usage: finalUsage,
|
|
919
|
+
finishReason: finalFinishReason
|
|
920
|
+
};
|
|
921
|
+
})();
|
|
922
|
+
done.catch(async (thrown) => {
|
|
923
|
+
const error = toError(thrown);
|
|
924
|
+
const duration = Date.now() - startedAt;
|
|
925
|
+
log.error("agent.stream error", {
|
|
926
|
+
name: config.name,
|
|
927
|
+
error: error.message,
|
|
928
|
+
duration
|
|
929
|
+
});
|
|
930
|
+
await fireHooks(log, wrapHook(config.onError, {
|
|
931
|
+
input,
|
|
932
|
+
error
|
|
933
|
+
}), wrapHook(readOverride(overrides, "onError"), {
|
|
934
|
+
input,
|
|
935
|
+
error
|
|
936
|
+
}));
|
|
937
|
+
});
|
|
938
|
+
return {
|
|
939
|
+
ok: true,
|
|
940
|
+
output: done.then((r) => r.output),
|
|
941
|
+
messages: done.then((r) => r.messages),
|
|
942
|
+
usage: done.then((r) => r.usage),
|
|
943
|
+
finishReason: done.then((r) => r.finishReason),
|
|
944
|
+
fullStream: readable
|
|
945
|
+
};
|
|
946
|
+
} catch (thrown) {
|
|
947
|
+
const error = toError(thrown);
|
|
948
|
+
const duration = Date.now() - startedAt;
|
|
949
|
+
log.error("agent.stream error", {
|
|
950
|
+
name: config.name,
|
|
951
|
+
error: error.message,
|
|
952
|
+
duration
|
|
953
|
+
});
|
|
954
|
+
await fireHooks(log, wrapHook(config.onError, {
|
|
955
|
+
input,
|
|
956
|
+
error
|
|
957
|
+
}), wrapHook(readOverride(overrides, "onError"), {
|
|
958
|
+
input,
|
|
959
|
+
error
|
|
960
|
+
}));
|
|
961
|
+
return {
|
|
962
|
+
ok: false,
|
|
963
|
+
error: {
|
|
964
|
+
code: "AGENT_ERROR",
|
|
965
|
+
message: error.message,
|
|
966
|
+
cause: error
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const agent = {
|
|
972
|
+
generate,
|
|
973
|
+
stream,
|
|
974
|
+
fn: () => generate
|
|
975
|
+
};
|
|
976
|
+
agent[RUNNABLE_META] = {
|
|
977
|
+
name: config.name,
|
|
978
|
+
inputSchema: config.input
|
|
979
|
+
};
|
|
980
|
+
return agent;
|
|
981
|
+
}
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/core/agents/flow/messages.ts
|
|
984
|
+
/**
|
|
985
|
+
* Build the `toolCallId` for a step.
|
|
986
|
+
*
|
|
987
|
+
* Combines the step id with the global step index to produce a unique
|
|
988
|
+
* identifier that correlates tool-call and tool-result messages.
|
|
989
|
+
*
|
|
990
|
+
* @param stepId - The step's user-provided id.
|
|
991
|
+
* @param index - The step's global index within the flow.
|
|
992
|
+
* @returns A unique tool call identifier.
|
|
993
|
+
*/
|
|
994
|
+
function buildToolCallId(stepId, index) {
|
|
995
|
+
return `${stepId}-${index}`;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Create an assistant message containing a synthetic tool-call part.
|
|
999
|
+
*
|
|
1000
|
+
* Emitted when a `$` step starts execution. The `input` field captures
|
|
1001
|
+
* the step's input snapshot (or `{}` when no input is available).
|
|
1002
|
+
*
|
|
1003
|
+
* @param toolCallId - Unique tool call identifier.
|
|
1004
|
+
* @param toolName - The step id used as the tool name.
|
|
1005
|
+
* @param args - The step's input snapshot.
|
|
1006
|
+
* @returns A `Message` with role `assistant` and a `tool-call` content part.
|
|
1007
|
+
*/
|
|
1008
|
+
function createToolCallMessage(toolCallId, toolName, args) {
|
|
1009
|
+
return {
|
|
1010
|
+
role: "assistant",
|
|
1011
|
+
content: [{
|
|
1012
|
+
type: "tool-call",
|
|
1013
|
+
toolCallId,
|
|
1014
|
+
toolName,
|
|
1015
|
+
input: args ?? {}
|
|
1016
|
+
}]
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Create a tool message containing a synthetic tool-result part.
|
|
1021
|
+
*
|
|
1022
|
+
* Emitted when a `$` step finishes execution. The `result` field captures
|
|
1023
|
+
* the step's output. When `isError` is true, the result represents an
|
|
1024
|
+
* error message.
|
|
1025
|
+
*
|
|
1026
|
+
* @param toolCallId - Unique tool call identifier (must match the paired tool-call).
|
|
1027
|
+
* @param toolName - The step id used as the tool name.
|
|
1028
|
+
* @param result - The step's output snapshot.
|
|
1029
|
+
* @param isError - Whether this result represents an error.
|
|
1030
|
+
* @returns A `Message` with role `tool` and a `tool-result` content part.
|
|
1031
|
+
*/
|
|
1032
|
+
function createToolResultMessage(toolCallId, toolName, result, isError) {
|
|
1033
|
+
return {
|
|
1034
|
+
role: "tool",
|
|
1035
|
+
content: [{
|
|
1036
|
+
type: "tool-result",
|
|
1037
|
+
toolCallId,
|
|
1038
|
+
toolName,
|
|
1039
|
+
output: result ?? {},
|
|
1040
|
+
...isError ? { isError: true } : {}
|
|
1041
|
+
}]
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Safely serialize a value to a string for message content.
|
|
1046
|
+
*
|
|
1047
|
+
* Returns the value as-is when it's already a string. Otherwise
|
|
1048
|
+
* delegates to {@link safeStringify} which handles circular refs,
|
|
1049
|
+
* Maps, Sets, bigints, and other non-JSON-serializable types.
|
|
1050
|
+
*
|
|
1051
|
+
* @param value - The value to serialize.
|
|
1052
|
+
* @returns A string representation of the value.
|
|
1053
|
+
*/
|
|
1054
|
+
function serializeMessageContent(value) {
|
|
1055
|
+
if (typeof value === "string") return value;
|
|
1056
|
+
return safeStringify(value ?? null);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Create a user message from flow agent input.
|
|
1060
|
+
*
|
|
1061
|
+
* This is the first message in the flow's message array, representing
|
|
1062
|
+
* the input passed to `flowAgent.generate()` or `flowAgent.stream()`.
|
|
1063
|
+
*
|
|
1064
|
+
* @param input - The flow agent input.
|
|
1065
|
+
* @returns A `Message` with role `user`.
|
|
1066
|
+
*/
|
|
1067
|
+
function createUserMessage(input) {
|
|
1068
|
+
return {
|
|
1069
|
+
role: "user",
|
|
1070
|
+
content: serializeMessageContent(input)
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Create a final assistant message from flow agent output.
|
|
1075
|
+
*
|
|
1076
|
+
* This is the last message in the flow's message array, representing
|
|
1077
|
+
* the validated output returned by the handler.
|
|
1078
|
+
*
|
|
1079
|
+
* @param output - The flow agent output.
|
|
1080
|
+
* @returns A `Message` with role `assistant`.
|
|
1081
|
+
*/
|
|
1082
|
+
function createAssistantMessage(output) {
|
|
1083
|
+
return {
|
|
1084
|
+
role: "assistant",
|
|
1085
|
+
content: serializeMessageContent(output)
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Collect text content from assistant messages in the message array.
|
|
1090
|
+
*
|
|
1091
|
+
* Used for flow agents without an output schema — the output is
|
|
1092
|
+
* the concatenated text from sub-agent responses. Only considers
|
|
1093
|
+
* messages with string content (tool-call messages have array content
|
|
1094
|
+
* and are skipped).
|
|
1095
|
+
*
|
|
1096
|
+
* @param messages - The flow's message array.
|
|
1097
|
+
* @returns The concatenated assistant text, trimmed.
|
|
1098
|
+
*/
|
|
1099
|
+
function collectTextFromMessages(messages) {
|
|
1100
|
+
return messages.filter((m) => m.role === "assistant" && typeof m.content === "string").map((m) => m.content).join("\n").trim();
|
|
1101
|
+
}
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region src/core/agents/flow/steps/factory.ts
|
|
1104
|
+
/**
|
|
1105
|
+
* Create a `StepBuilder` (`$`) instance.
|
|
1106
|
+
*
|
|
1107
|
+
* The returned builder is the `$` object passed into every flow agent
|
|
1108
|
+
* handler and step callback. It owns the full step lifecycle:
|
|
1109
|
+
* trace registration, hook firing, error wrapping,
|
|
1110
|
+
* and `StepResult<T>` construction.
|
|
1111
|
+
*
|
|
1112
|
+
* @param options - Factory configuration with context, optional parent
|
|
1113
|
+
* hooks, and optional stream writer.
|
|
1114
|
+
* @returns A `StepBuilder` instance.
|
|
1115
|
+
*/
|
|
1116
|
+
function createStepBuilder(options) {
|
|
1117
|
+
return createStepBuilderInternal(options, { current: 0 });
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Internal factory that accepts a shared index ref.
|
|
1121
|
+
*
|
|
1122
|
+
* Child builders (created for nested `$` in callbacks) share
|
|
1123
|
+
* the same ref so step indices are globally unique.
|
|
1124
|
+
*/
|
|
1125
|
+
function createStepBuilderInternal(options, indexRef) {
|
|
1126
|
+
const { ctx, parentHooks, writer } = options;
|
|
1127
|
+
/**
|
|
1128
|
+
* Core step primitive — every other method delegates here.
|
|
1129
|
+
*/
|
|
1130
|
+
async function step(config) {
|
|
1131
|
+
const onFinishHandler = buildOnFinishHandler(config.onFinish);
|
|
1132
|
+
return executeStep({
|
|
1133
|
+
id: config.id,
|
|
1134
|
+
type: "step",
|
|
1135
|
+
execute: config.execute,
|
|
1136
|
+
onStart: config.onStart,
|
|
1137
|
+
onFinish: onFinishHandler,
|
|
1138
|
+
onError: config.onError
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Shared lifecycle for all step types.
|
|
1143
|
+
*/
|
|
1144
|
+
async function executeStep(params) {
|
|
1145
|
+
const { id, type, execute, input, onStart, onFinish, onError } = params;
|
|
1146
|
+
const stepInfo = {
|
|
1147
|
+
id,
|
|
1148
|
+
index: indexRef.current++,
|
|
1149
|
+
type
|
|
1150
|
+
};
|
|
1151
|
+
const startedAt = Date.now();
|
|
1152
|
+
const childTrace = [];
|
|
1153
|
+
const child$ = createStepBuilderInternal({
|
|
1154
|
+
ctx: {
|
|
1155
|
+
signal: ctx.signal,
|
|
1156
|
+
log: ctx.log.child({ stepId: id }),
|
|
1157
|
+
trace: childTrace,
|
|
1158
|
+
messages: ctx.messages
|
|
1159
|
+
},
|
|
1160
|
+
parentHooks,
|
|
1161
|
+
writer
|
|
1162
|
+
}, indexRef);
|
|
1163
|
+
const toolCallId = buildToolCallId(id, stepInfo.index);
|
|
1164
|
+
ctx.messages.push(createToolCallMessage(toolCallId, id, input));
|
|
1165
|
+
if (writer != null) writer.write({
|
|
1166
|
+
type: "tool-call",
|
|
1167
|
+
toolCallId,
|
|
1168
|
+
toolName: id,
|
|
1169
|
+
input: input ?? {}
|
|
1170
|
+
}).catch((err) => {
|
|
1171
|
+
ctx.log.warn({
|
|
1172
|
+
err,
|
|
1173
|
+
toolCallId
|
|
1174
|
+
}, "failed to write tool-call event to stream");
|
|
1175
|
+
});
|
|
1176
|
+
const onStartHook = buildHookCallback(onStart, (fn) => fn({ id }));
|
|
1177
|
+
const parentOnStepStartHook = buildParentHookCallback(parentHooks, "onStepStart", (fn) => fn({ step: stepInfo }));
|
|
1178
|
+
await fireHooks(ctx.log, onStartHook, parentOnStepStartHook);
|
|
1179
|
+
try {
|
|
1180
|
+
const value = await execute({ $: child$ });
|
|
1181
|
+
const finishedAt = Date.now();
|
|
1182
|
+
const duration = finishedAt - startedAt;
|
|
1183
|
+
const usage = value != null && typeof value === "object" && "usage" in value ? value.usage : void 0;
|
|
1184
|
+
ctx.trace.push({
|
|
1185
|
+
id,
|
|
1186
|
+
type,
|
|
1187
|
+
input,
|
|
1188
|
+
startedAt,
|
|
1189
|
+
finishedAt,
|
|
1190
|
+
children: childTrace,
|
|
1191
|
+
output: value,
|
|
1192
|
+
...usage != null ? { usage } : {}
|
|
1193
|
+
});
|
|
1194
|
+
ctx.messages.push(createToolResultMessage(toolCallId, id, value));
|
|
1195
|
+
if (writer != null) writer.write({
|
|
1196
|
+
type: "tool-result",
|
|
1197
|
+
toolCallId,
|
|
1198
|
+
toolName: id,
|
|
1199
|
+
input: input ?? {},
|
|
1200
|
+
output: value ?? null
|
|
1201
|
+
}).catch((err) => {
|
|
1202
|
+
ctx.log.warn({
|
|
1203
|
+
err,
|
|
1204
|
+
toolCallId
|
|
1205
|
+
}, "failed to write tool-result event to stream");
|
|
1206
|
+
});
|
|
1207
|
+
const onFinishHook = buildHookCallback(onFinish, (fn) => fn({
|
|
1208
|
+
id,
|
|
1209
|
+
result: value,
|
|
1210
|
+
duration
|
|
1211
|
+
}));
|
|
1212
|
+
const parentOnStepFinishHook = buildParentHookCallback(parentHooks, "onStepFinish", (fn) => fn({
|
|
1213
|
+
step: stepInfo,
|
|
1214
|
+
result: value,
|
|
1215
|
+
duration
|
|
1216
|
+
}));
|
|
1217
|
+
await fireHooks(ctx.log, onFinishHook, parentOnStepFinishHook);
|
|
1218
|
+
return {
|
|
1219
|
+
ok: true,
|
|
1220
|
+
value,
|
|
1221
|
+
step: stepInfo,
|
|
1222
|
+
duration
|
|
1223
|
+
};
|
|
1224
|
+
} catch (thrown) {
|
|
1225
|
+
const error = toError(thrown);
|
|
1226
|
+
const finishedAt = Date.now();
|
|
1227
|
+
const duration = finishedAt - startedAt;
|
|
1228
|
+
ctx.trace.push({
|
|
1229
|
+
id,
|
|
1230
|
+
type,
|
|
1231
|
+
input,
|
|
1232
|
+
startedAt,
|
|
1233
|
+
finishedAt,
|
|
1234
|
+
children: childTrace,
|
|
1235
|
+
error
|
|
1236
|
+
});
|
|
1237
|
+
const stepError = {
|
|
1238
|
+
code: "STEP_ERROR",
|
|
1239
|
+
message: error.message,
|
|
1240
|
+
cause: error,
|
|
1241
|
+
stepId: id
|
|
1242
|
+
};
|
|
1243
|
+
ctx.messages.push(createToolResultMessage(toolCallId, id, { error: error.message }, true));
|
|
1244
|
+
if (writer != null) writer.write({
|
|
1245
|
+
type: "tool-result",
|
|
1246
|
+
toolCallId,
|
|
1247
|
+
toolName: id,
|
|
1248
|
+
input: input ?? {},
|
|
1249
|
+
output: { error: error.message }
|
|
1250
|
+
}).catch((err) => {
|
|
1251
|
+
ctx.log.warn({
|
|
1252
|
+
err,
|
|
1253
|
+
toolCallId
|
|
1254
|
+
}, "failed to write error tool-result event to stream");
|
|
1255
|
+
});
|
|
1256
|
+
const onErrorHook = buildHookCallback(onError, (fn) => fn({
|
|
1257
|
+
id,
|
|
1258
|
+
error
|
|
1259
|
+
}));
|
|
1260
|
+
const parentOnStepFinishHook = buildParentHookCallback(parentHooks, "onStepFinish", (fn) => fn({
|
|
1261
|
+
step: stepInfo,
|
|
1262
|
+
result: void 0,
|
|
1263
|
+
duration
|
|
1264
|
+
}));
|
|
1265
|
+
await fireHooks(ctx.log, onErrorHook, parentOnStepFinishHook);
|
|
1266
|
+
return {
|
|
1267
|
+
ok: false,
|
|
1268
|
+
error: stepError,
|
|
1269
|
+
step: stepInfo,
|
|
1270
|
+
duration
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
async function agent(config) {
|
|
1275
|
+
const onFinishHandler = buildOnFinishHandlerWithCast(config.onFinish);
|
|
1276
|
+
return executeStep({
|
|
1277
|
+
id: config.id,
|
|
1278
|
+
type: "agent",
|
|
1279
|
+
input: config.input,
|
|
1280
|
+
execute: async () => {
|
|
1281
|
+
const agentConfig = {
|
|
1282
|
+
signal: ctx.signal,
|
|
1283
|
+
...config.config,
|
|
1284
|
+
logger: ctx.log.child({ stepId: config.id })
|
|
1285
|
+
};
|
|
1286
|
+
if (config.stream && writer != null) {
|
|
1287
|
+
const streamResult = await config.agent.stream(config.input, agentConfig);
|
|
1288
|
+
if (!streamResult.ok) throw streamResult.error.cause ?? new Error(streamResult.error.message);
|
|
1289
|
+
const full = streamResult;
|
|
1290
|
+
for await (const part of full.fullStream) if (part.type === "text-delta") await writer.write(part);
|
|
1291
|
+
return {
|
|
1292
|
+
output: await full.output,
|
|
1293
|
+
messages: await full.messages,
|
|
1294
|
+
usage: await full.usage,
|
|
1295
|
+
finishReason: await full.finishReason
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
const result = await config.agent.generate(config.input, agentConfig);
|
|
1299
|
+
if (!result.ok) throw result.error.cause ?? new Error(result.error.message);
|
|
1300
|
+
const full = result;
|
|
1301
|
+
return {
|
|
1302
|
+
output: full.output,
|
|
1303
|
+
messages: full.messages,
|
|
1304
|
+
usage: full.usage,
|
|
1305
|
+
finishReason: full.finishReason
|
|
1306
|
+
};
|
|
1307
|
+
},
|
|
1308
|
+
onStart: config.onStart,
|
|
1309
|
+
onFinish: onFinishHandler,
|
|
1310
|
+
onError: config.onError
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
async function map(config) {
|
|
1314
|
+
const onFinishHandler = buildOnFinishHandlerWithCast(config.onFinish);
|
|
1315
|
+
return executeStep({
|
|
1316
|
+
id: config.id,
|
|
1317
|
+
type: "map",
|
|
1318
|
+
input: config.input,
|
|
1319
|
+
execute: async ({ $ }) => {
|
|
1320
|
+
const concurrency = config.concurrency;
|
|
1321
|
+
if (concurrency != null && concurrency !== Infinity) return poolMap(config.input, concurrency, ctx.signal, (item, index) => config.execute({
|
|
1322
|
+
item,
|
|
1323
|
+
index,
|
|
1324
|
+
$
|
|
1325
|
+
}));
|
|
1326
|
+
return Promise.all(config.input.map((item, index) => config.execute({
|
|
1327
|
+
item,
|
|
1328
|
+
index,
|
|
1329
|
+
$
|
|
1330
|
+
})));
|
|
1331
|
+
},
|
|
1332
|
+
onStart: config.onStart,
|
|
1333
|
+
onFinish: onFinishHandler,
|
|
1334
|
+
onError: config.onError
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
async function each(config) {
|
|
1338
|
+
const onFinishHandler = buildOnFinishHandlerVoid(config.onFinish);
|
|
1339
|
+
return executeStep({
|
|
1340
|
+
id: config.id,
|
|
1341
|
+
type: "each",
|
|
1342
|
+
input: config.input,
|
|
1343
|
+
execute: async ({ $ }) => {
|
|
1344
|
+
for (const [i, item] of config.input.entries()) {
|
|
1345
|
+
if (ctx.signal.aborted) throw new Error("Aborted");
|
|
1346
|
+
await config.execute({
|
|
1347
|
+
item,
|
|
1348
|
+
index: i,
|
|
1349
|
+
$
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
onStart: config.onStart,
|
|
1354
|
+
onFinish: onFinishHandler,
|
|
1355
|
+
onError: config.onError
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
async function reduce(config) {
|
|
1359
|
+
const onFinishHandler = buildOnFinishHandlerWithCast(config.onFinish);
|
|
1360
|
+
return executeStep({
|
|
1361
|
+
id: config.id,
|
|
1362
|
+
type: "reduce",
|
|
1363
|
+
input: config.input,
|
|
1364
|
+
execute: async ({ $ }) => {
|
|
1365
|
+
return await reduceSequential(config.input, config.initial, ctx.signal, (item, accumulator, index) => config.execute({
|
|
1366
|
+
item,
|
|
1367
|
+
accumulator,
|
|
1368
|
+
index,
|
|
1369
|
+
$
|
|
1370
|
+
}));
|
|
1371
|
+
},
|
|
1372
|
+
onStart: config.onStart,
|
|
1373
|
+
onFinish: onFinishHandler,
|
|
1374
|
+
onError: config.onError
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
async function whileStep(config) {
|
|
1378
|
+
const onFinishHandler = buildOnFinishHandlerWithCast(config.onFinish);
|
|
1379
|
+
return executeStep({
|
|
1380
|
+
id: config.id,
|
|
1381
|
+
type: "while",
|
|
1382
|
+
execute: async ({ $ }) => {
|
|
1383
|
+
return await whileSequential(config.condition, ctx.signal, (index) => config.execute({
|
|
1384
|
+
index,
|
|
1385
|
+
$
|
|
1386
|
+
}));
|
|
1387
|
+
},
|
|
1388
|
+
onStart: config.onStart,
|
|
1389
|
+
onFinish: onFinishHandler,
|
|
1390
|
+
onError: config.onError
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
async function all(config) {
|
|
1394
|
+
const onFinishHandler = buildOnFinishHandlerWithCast(config.onFinish);
|
|
1395
|
+
return executeStep({
|
|
1396
|
+
id: config.id,
|
|
1397
|
+
type: "all",
|
|
1398
|
+
execute: async ({ $ }) => {
|
|
1399
|
+
const ac = new AbortController();
|
|
1400
|
+
const onAbort = () => ac.abort();
|
|
1401
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
1402
|
+
try {
|
|
1403
|
+
return await Promise.all(config.entries.map((factory) => factory(ac.signal, $)));
|
|
1404
|
+
} catch (err) {
|
|
1405
|
+
ac.abort();
|
|
1406
|
+
throw err;
|
|
1407
|
+
} finally {
|
|
1408
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
1409
|
+
}
|
|
1410
|
+
},
|
|
1411
|
+
onStart: config.onStart,
|
|
1412
|
+
onFinish: onFinishHandler,
|
|
1413
|
+
onError: config.onError
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
async function race(config) {
|
|
1417
|
+
const onFinishHandler = buildOnFinishHandlerRace(config.onFinish);
|
|
1418
|
+
return executeStep({
|
|
1419
|
+
id: config.id,
|
|
1420
|
+
type: "race",
|
|
1421
|
+
execute: async ({ $ }) => {
|
|
1422
|
+
const ac = new AbortController();
|
|
1423
|
+
const onAbort = () => ac.abort();
|
|
1424
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
1425
|
+
try {
|
|
1426
|
+
return await Promise.race(config.entries.map((factory) => factory(ac.signal, $)));
|
|
1427
|
+
} finally {
|
|
1428
|
+
ac.abort();
|
|
1429
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
onStart: config.onStart,
|
|
1433
|
+
onFinish: onFinishHandler,
|
|
1434
|
+
onError: config.onError
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
step,
|
|
1439
|
+
agent,
|
|
1440
|
+
map,
|
|
1441
|
+
each,
|
|
1442
|
+
reduce,
|
|
1443
|
+
while: whileStep,
|
|
1444
|
+
all,
|
|
1445
|
+
race
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function buildHookCallback(handler, invoke) {
|
|
1449
|
+
if (handler != null) return () => invoke(handler);
|
|
1450
|
+
}
|
|
1451
|
+
function buildParentHookCallback(hooks, key, invoke) {
|
|
1452
|
+
if (hooks == null) return;
|
|
1453
|
+
const fn = hooks[key];
|
|
1454
|
+
if (fn == null) return;
|
|
1455
|
+
return () => invoke(fn);
|
|
1456
|
+
}
|
|
1457
|
+
function buildOnFinishHandler(onFinish) {
|
|
1458
|
+
if (onFinish == null) return;
|
|
1459
|
+
return (event) => onFinish({
|
|
1460
|
+
id: event.id,
|
|
1461
|
+
result: event.result,
|
|
1462
|
+
duration: event.duration
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
function buildOnFinishHandlerWithCast(onFinish) {
|
|
1466
|
+
if (onFinish == null) return;
|
|
1467
|
+
return (event) => onFinish({
|
|
1468
|
+
id: event.id,
|
|
1469
|
+
result: event.result,
|
|
1470
|
+
duration: event.duration
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
function buildOnFinishHandlerVoid(onFinish) {
|
|
1474
|
+
if (onFinish == null) return;
|
|
1475
|
+
return (event) => onFinish({
|
|
1476
|
+
id: event.id,
|
|
1477
|
+
duration: event.duration
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
function buildOnFinishHandlerRace(onFinish) {
|
|
1481
|
+
if (onFinish == null) return;
|
|
1482
|
+
return (event) => onFinish({
|
|
1483
|
+
id: event.id,
|
|
1484
|
+
result: event.result,
|
|
1485
|
+
duration: event.duration
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
async function reduceSequential(items, initial, signal, fn) {
|
|
1489
|
+
async function loop(accumulator, index) {
|
|
1490
|
+
if (index >= items.length) return accumulator;
|
|
1491
|
+
if (signal.aborted) throw new Error("Aborted");
|
|
1492
|
+
return loop(await fn(items[index], accumulator, index), index + 1);
|
|
1493
|
+
}
|
|
1494
|
+
return loop(initial, 0);
|
|
1495
|
+
}
|
|
1496
|
+
async function whileSequential(condition, signal, fn) {
|
|
1497
|
+
async function loop(value, index) {
|
|
1498
|
+
if (!condition({
|
|
1499
|
+
value,
|
|
1500
|
+
index
|
|
1501
|
+
})) return value;
|
|
1502
|
+
if (signal.aborted) throw new Error("Aborted");
|
|
1503
|
+
return loop(await fn(index), index + 1);
|
|
1504
|
+
}
|
|
1505
|
+
return loop(void 0, 0);
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Worker-pool map with bounded concurrency.
|
|
1509
|
+
*
|
|
1510
|
+
* Runs `fn` over `items` with at most `concurrency` concurrent
|
|
1511
|
+
* executions. Results are returned in input order.
|
|
1512
|
+
*/
|
|
1513
|
+
async function poolMap(items, concurrency, signal, fn) {
|
|
1514
|
+
const results = Array.from({ length: items.length });
|
|
1515
|
+
const indexRef = { current: 0 };
|
|
1516
|
+
async function worker() {
|
|
1517
|
+
while (indexRef.current < items.length) {
|
|
1518
|
+
if (signal.aborted) throw new Error("Aborted");
|
|
1519
|
+
const i = indexRef.current++;
|
|
1520
|
+
results[i] = await fn(items[i], i);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
|
|
1524
|
+
await Promise.all(workers);
|
|
1525
|
+
return results;
|
|
1526
|
+
}
|
|
1527
|
+
//#endregion
|
|
1528
|
+
//#region src/core/provider/usage.ts
|
|
1529
|
+
/**
|
|
1530
|
+
* Aggregate token counts across multiple raw tracking records.
|
|
1531
|
+
*
|
|
1532
|
+
* Sums each field, treating `undefined` as `0`.
|
|
1533
|
+
*/
|
|
1534
|
+
function aggregateTokens(usages) {
|
|
1535
|
+
return {
|
|
1536
|
+
inputTokens: sumBy(usages, (u) => u.inputTokens ?? 0),
|
|
1537
|
+
outputTokens: sumBy(usages, (u) => u.outputTokens ?? 0),
|
|
1538
|
+
totalTokens: sumBy(usages, (u) => u.totalTokens ?? 0),
|
|
1539
|
+
cacheReadTokens: sumBy(usages, (u) => u.cacheReadTokens ?? 0),
|
|
1540
|
+
cacheWriteTokens: sumBy(usages, (u) => u.cacheWriteTokens ?? 0),
|
|
1541
|
+
reasoningTokens: sumBy(usages, (u) => u.reasoningTokens ?? 0)
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Compute final usage for a single agent call.
|
|
1546
|
+
*
|
|
1547
|
+
* Aggregates token counts from one or more raw tracking records.
|
|
1548
|
+
* Returns a flat object with tokens + agentId.
|
|
1549
|
+
*
|
|
1550
|
+
* @param agentId - The agent that produced these records.
|
|
1551
|
+
* @param records - Raw tracking records from the agent's execution.
|
|
1552
|
+
* @returns Flat {@link AgentTokenUsage} with resolved token counts.
|
|
1553
|
+
*/
|
|
1554
|
+
function agentUsage(agentId, records) {
|
|
1555
|
+
return {
|
|
1556
|
+
agentId,
|
|
1557
|
+
...aggregateTokens(match(records).when(Array.isArray, (r) => r).otherwise((r) => [r]))
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Compute final usage for a flow agent with multiple agent calls.
|
|
1562
|
+
*
|
|
1563
|
+
* Groups raw tracking records by `source.agentId`, computes per-agent
|
|
1564
|
+
* usage via {@link agentUsage}.
|
|
1565
|
+
*
|
|
1566
|
+
* @param records - Raw tracking records from all agents in the flow.
|
|
1567
|
+
* @returns {@link FlowAgentTokenUsage} with per-agent breakdown.
|
|
1568
|
+
*/
|
|
1569
|
+
function flowAgentUsage(records) {
|
|
1570
|
+
const grouped = groupBy(records, (r) => match(r.source).with(P.nonNullable, (s) => match(s.agentId).with(P.string, (id) => id).otherwise(() => "unknown")).otherwise(() => "unknown"));
|
|
1571
|
+
return { usages: Object.entries(grouped).map(([id, group]) => agentUsage(id, group)) };
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Sum multiple {@link TokenUsage} objects field-by-field.
|
|
1575
|
+
*
|
|
1576
|
+
* Pure function — returns a new object without mutating any input.
|
|
1577
|
+
* Returns zero-valued usage when given an empty array.
|
|
1578
|
+
*
|
|
1579
|
+
* @param usages - Array of usage objects to sum.
|
|
1580
|
+
* @returns A new `TokenUsage` with each field summed.
|
|
1581
|
+
*/
|
|
1582
|
+
function sumTokenUsage(usages) {
|
|
1583
|
+
return {
|
|
1584
|
+
inputTokens: sumBy(usages, (u) => u.inputTokens),
|
|
1585
|
+
outputTokens: sumBy(usages, (u) => u.outputTokens),
|
|
1586
|
+
totalTokens: sumBy(usages, (u) => u.totalTokens),
|
|
1587
|
+
cacheReadTokens: sumBy(usages, (u) => u.cacheReadTokens),
|
|
1588
|
+
cacheWriteTokens: sumBy(usages, (u) => u.cacheWriteTokens),
|
|
1589
|
+
reasoningTokens: sumBy(usages, (u) => u.reasoningTokens)
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
//#endregion
|
|
1593
|
+
//#region src/lib/trace.ts
|
|
1594
|
+
/**
|
|
1595
|
+
* Recursively collect all {@link TokenUsage} values from a trace tree.
|
|
1596
|
+
*
|
|
1597
|
+
* Walks every entry (including nested children) and returns a flat
|
|
1598
|
+
* array of usage objects. Entries without usage are skipped.
|
|
1599
|
+
*
|
|
1600
|
+
* @param trace - The trace array to collect from.
|
|
1601
|
+
* @returns Flat array of {@link TokenUsage} values found in the tree.
|
|
1602
|
+
*/
|
|
1603
|
+
function collectUsages(trace) {
|
|
1604
|
+
return trace.flatMap((entry) => {
|
|
1605
|
+
const usages = match(entry.usage).with(P.nonNullable, (u) => [u]).otherwise(() => []);
|
|
1606
|
+
if (entry.children != null && entry.children.length > 0) return [...usages, ...collectUsages(entry.children)];
|
|
1607
|
+
return usages;
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Recursively deep-clone and freeze a trace array.
|
|
1612
|
+
*
|
|
1613
|
+
* Returns a structurally identical tree that is `Object.freeze`d at
|
|
1614
|
+
* every level, preventing post-run mutation of the result trace.
|
|
1615
|
+
*
|
|
1616
|
+
* @internal
|
|
1617
|
+
*/
|
|
1618
|
+
function snapshotTrace(trace) {
|
|
1619
|
+
return Object.freeze(trace.map((entry) => {
|
|
1620
|
+
const childSpread = match(match(entry.children).with(P.nonNullable, (c) => snapshotTrace(c)).otherwise(() => void 0)).with(P.nonNullable, (c) => ({ children: c })).otherwise(() => ({}));
|
|
1621
|
+
return Object.freeze({
|
|
1622
|
+
...entry,
|
|
1623
|
+
...childSpread
|
|
1624
|
+
});
|
|
1625
|
+
}));
|
|
1626
|
+
}
|
|
1627
|
+
//#endregion
|
|
1628
|
+
//#region src/core/agents/flow/flow-agent.ts
|
|
1629
|
+
/**
|
|
1630
|
+
* Resolve the logger for a single flow agent execution.
|
|
1631
|
+
*
|
|
1632
|
+
* @private
|
|
1633
|
+
*/
|
|
1634
|
+
function resolveFlowAgentLogger(base, flowAgentId, overrides) {
|
|
1635
|
+
return ((overrides && overrides.logger) ?? base).child({ flowAgentId });
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Augment the step builder with custom steps from the engine.
|
|
1639
|
+
*
|
|
1640
|
+
* @private
|
|
1641
|
+
*/
|
|
1642
|
+
function augmentStepBuilder(base, ctx, internal) {
|
|
1643
|
+
if (internal && internal.augment$) return internal.augment$(base, ctx);
|
|
1644
|
+
return base;
|
|
1645
|
+
}
|
|
1646
|
+
function flowAgent(config, handler, _internal) {
|
|
1647
|
+
const baseLogger = config.logger ?? createDefaultLogger();
|
|
1648
|
+
/**
|
|
1649
|
+
* Resolve the handler output into a final value, validating against
|
|
1650
|
+
* the output schema when present. Also pushes the assistant message.
|
|
1651
|
+
*
|
|
1652
|
+
* Returns `{ ok: true, value }` on success, or `{ ok: false, message }`
|
|
1653
|
+
* when output validation fails.
|
|
1654
|
+
*
|
|
1655
|
+
* @private
|
|
1656
|
+
*/
|
|
1657
|
+
function resolveFlowOutput(output, messages) {
|
|
1658
|
+
if (config.output !== void 0) {
|
|
1659
|
+
const outputParsed = config.output.safeParse(output);
|
|
1660
|
+
if (!outputParsed.success) return {
|
|
1661
|
+
ok: false,
|
|
1662
|
+
message: `Output validation failed: ${outputParsed.error.message}`
|
|
1663
|
+
};
|
|
1664
|
+
messages.push(createAssistantMessage(outputParsed.data));
|
|
1665
|
+
return {
|
|
1666
|
+
ok: true,
|
|
1667
|
+
value: outputParsed.data
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
const text = collectTextFromMessages(messages);
|
|
1671
|
+
messages.push(createAssistantMessage(text));
|
|
1672
|
+
return {
|
|
1673
|
+
ok: true,
|
|
1674
|
+
value: text
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
async function generate(input, overrides) {
|
|
1678
|
+
const inputParsed = config.input.safeParse(input);
|
|
1679
|
+
if (!inputParsed.success) return {
|
|
1680
|
+
ok: false,
|
|
1681
|
+
error: {
|
|
1682
|
+
code: "VALIDATION_ERROR",
|
|
1683
|
+
message: `Input validation failed: ${inputParsed.error.message}`
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
const parsedInput = inputParsed.data;
|
|
1687
|
+
const startedAt = Date.now();
|
|
1688
|
+
const log = resolveFlowAgentLogger(baseLogger, config.name, overrides);
|
|
1689
|
+
const signal = overrides && overrides.signal || new AbortController().signal;
|
|
1690
|
+
const trace = [];
|
|
1691
|
+
const messages = [];
|
|
1692
|
+
const ctx = {
|
|
1693
|
+
signal,
|
|
1694
|
+
log,
|
|
1695
|
+
trace,
|
|
1696
|
+
messages
|
|
1697
|
+
};
|
|
1698
|
+
const $ = augmentStepBuilder(createStepBuilder({
|
|
1699
|
+
ctx,
|
|
1700
|
+
parentHooks: {
|
|
1701
|
+
onStepStart: config.onStepStart,
|
|
1702
|
+
onStepFinish: config.onStepFinish
|
|
1703
|
+
}
|
|
1704
|
+
}), ctx, _internal);
|
|
1705
|
+
messages.push(createUserMessage(parsedInput));
|
|
1706
|
+
await fireHooks(log, wrapHook(config.onStart, { input: parsedInput }), wrapHook(overrides && overrides.onStart, { input: parsedInput }));
|
|
1707
|
+
log.debug("flowAgent.generate start", { name: config.name });
|
|
1708
|
+
try {
|
|
1709
|
+
const outputResult = resolveFlowOutput(await handler({
|
|
1710
|
+
input: parsedInput,
|
|
1711
|
+
$,
|
|
1712
|
+
log
|
|
1713
|
+
}), messages);
|
|
1714
|
+
if (!outputResult.ok) return {
|
|
1715
|
+
ok: false,
|
|
1716
|
+
error: {
|
|
1717
|
+
code: "VALIDATION_ERROR",
|
|
1718
|
+
message: outputResult.message
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
const resolvedOutput = outputResult.value;
|
|
1722
|
+
const duration = Date.now() - startedAt;
|
|
1723
|
+
const usage = sumTokenUsage(collectUsages(trace));
|
|
1724
|
+
const frozenTrace = snapshotTrace(trace);
|
|
1725
|
+
const result = {
|
|
1726
|
+
output: resolvedOutput,
|
|
1727
|
+
messages: [...messages],
|
|
1728
|
+
usage,
|
|
1729
|
+
finishReason: "stop",
|
|
1730
|
+
trace: frozenTrace,
|
|
1731
|
+
duration
|
|
1732
|
+
};
|
|
1733
|
+
await fireHooks(log, wrapHook(config.onFinish, {
|
|
1734
|
+
input: parsedInput,
|
|
1735
|
+
result,
|
|
1736
|
+
duration
|
|
1737
|
+
}), wrapHook(overrides && overrides.onFinish, {
|
|
1738
|
+
input: parsedInput,
|
|
1739
|
+
result,
|
|
1740
|
+
duration
|
|
1741
|
+
}));
|
|
1742
|
+
log.debug("flowAgent.generate finish", {
|
|
1743
|
+
name: config.name,
|
|
1744
|
+
duration
|
|
1745
|
+
});
|
|
1746
|
+
return {
|
|
1747
|
+
ok: true,
|
|
1748
|
+
...result
|
|
1749
|
+
};
|
|
1750
|
+
} catch (thrown) {
|
|
1751
|
+
const error = toError(thrown);
|
|
1752
|
+
const duration = Date.now() - startedAt;
|
|
1753
|
+
log.error("flowAgent.generate error", {
|
|
1754
|
+
name: config.name,
|
|
1755
|
+
error: error.message,
|
|
1756
|
+
duration
|
|
1757
|
+
});
|
|
1758
|
+
await fireHooks(log, wrapHook(config.onError, {
|
|
1759
|
+
input: parsedInput,
|
|
1760
|
+
error
|
|
1761
|
+
}), wrapHook(overrides && overrides.onError, {
|
|
1762
|
+
input: parsedInput,
|
|
1763
|
+
error
|
|
1764
|
+
}));
|
|
1765
|
+
return {
|
|
1766
|
+
ok: false,
|
|
1767
|
+
error: {
|
|
1768
|
+
code: "FLOW_AGENT_ERROR",
|
|
1769
|
+
message: error.message,
|
|
1770
|
+
cause: error
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
async function stream(input, overrides) {
|
|
1776
|
+
const inputParsed = config.input.safeParse(input);
|
|
1777
|
+
if (!inputParsed.success) return {
|
|
1778
|
+
ok: false,
|
|
1779
|
+
error: {
|
|
1780
|
+
code: "VALIDATION_ERROR",
|
|
1781
|
+
message: `Input validation failed: ${inputParsed.error.message}`
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
const parsedInput = inputParsed.data;
|
|
1785
|
+
const startedAt = Date.now();
|
|
1786
|
+
const log = resolveFlowAgentLogger(baseLogger, config.name, overrides);
|
|
1787
|
+
const signal = overrides && overrides.signal || new AbortController().signal;
|
|
1788
|
+
const trace = [];
|
|
1789
|
+
const messages = [];
|
|
1790
|
+
const ctx = {
|
|
1791
|
+
signal,
|
|
1792
|
+
log,
|
|
1793
|
+
trace,
|
|
1794
|
+
messages
|
|
1795
|
+
};
|
|
1796
|
+
const { readable, writable } = new TransformStream();
|
|
1797
|
+
const writer = writable.getWriter();
|
|
1798
|
+
const $ = augmentStepBuilder(createStepBuilder({
|
|
1799
|
+
ctx,
|
|
1800
|
+
parentHooks: {
|
|
1801
|
+
onStepStart: config.onStepStart,
|
|
1802
|
+
onStepFinish: config.onStepFinish
|
|
1803
|
+
},
|
|
1804
|
+
writer
|
|
1805
|
+
}), ctx, _internal);
|
|
1806
|
+
messages.push(createUserMessage(parsedInput));
|
|
1807
|
+
await fireHooks(log, wrapHook(config.onStart, { input: parsedInput }), wrapHook(overrides && overrides.onStart, { input: parsedInput }));
|
|
1808
|
+
log.debug("flowAgent.stream start", { name: config.name });
|
|
1809
|
+
const done = (async () => {
|
|
1810
|
+
try {
|
|
1811
|
+
const outputResult = resolveFlowOutput(await handler({
|
|
1812
|
+
input: parsedInput,
|
|
1813
|
+
$,
|
|
1814
|
+
log
|
|
1815
|
+
}), messages);
|
|
1816
|
+
if (!outputResult.ok) throw new Error(outputResult.message);
|
|
1817
|
+
const resolvedOutput = outputResult.value;
|
|
1818
|
+
const duration = Date.now() - startedAt;
|
|
1819
|
+
const usage = sumTokenUsage(collectUsages(trace));
|
|
1820
|
+
const result = {
|
|
1821
|
+
output: resolvedOutput,
|
|
1822
|
+
messages: [...messages],
|
|
1823
|
+
usage,
|
|
1824
|
+
finishReason: "stop",
|
|
1825
|
+
trace: snapshotTrace(trace),
|
|
1826
|
+
duration
|
|
1827
|
+
};
|
|
1828
|
+
await fireHooks(log, wrapHook(config.onFinish, {
|
|
1829
|
+
input: parsedInput,
|
|
1830
|
+
result,
|
|
1831
|
+
duration
|
|
1832
|
+
}), wrapHook(overrides && overrides.onFinish, {
|
|
1833
|
+
input: parsedInput,
|
|
1834
|
+
result,
|
|
1835
|
+
duration
|
|
1836
|
+
}));
|
|
1837
|
+
log.debug("flowAgent.stream finish", {
|
|
1838
|
+
name: config.name,
|
|
1839
|
+
duration
|
|
1840
|
+
});
|
|
1841
|
+
await writer.write({
|
|
1842
|
+
type: "finish",
|
|
1843
|
+
finishReason: "stop",
|
|
1844
|
+
rawFinishReason: void 0,
|
|
1845
|
+
totalUsage: {
|
|
1846
|
+
inputTokens: usage.inputTokens,
|
|
1847
|
+
outputTokens: usage.outputTokens,
|
|
1848
|
+
totalTokens: usage.totalTokens
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
await writer.close();
|
|
1852
|
+
return result;
|
|
1853
|
+
} catch (thrown) {
|
|
1854
|
+
const error = toError(thrown);
|
|
1855
|
+
const duration = Date.now() - startedAt;
|
|
1856
|
+
log.error("flowAgent.stream error", {
|
|
1857
|
+
name: config.name,
|
|
1858
|
+
error: error.message,
|
|
1859
|
+
duration
|
|
1860
|
+
});
|
|
1861
|
+
/* v8 ignore start -- defensive; writer rarely rejects in practice */
|
|
1862
|
+
await writer.write({
|
|
1863
|
+
type: "error",
|
|
1864
|
+
error
|
|
1865
|
+
}).catch((err) => {
|
|
1866
|
+
log.debug("failed to write error event to stream", { err });
|
|
1867
|
+
});
|
|
1868
|
+
await writer.close().catch((err) => {
|
|
1869
|
+
log.debug("failed to close stream writer", { err });
|
|
1870
|
+
});
|
|
1871
|
+
/* v8 ignore stop */
|
|
1872
|
+
await fireHooks(log, wrapHook(config.onError, {
|
|
1873
|
+
input: parsedInput,
|
|
1874
|
+
error
|
|
1875
|
+
}), wrapHook(overrides && overrides.onError, {
|
|
1876
|
+
input: parsedInput,
|
|
1877
|
+
error
|
|
1878
|
+
}));
|
|
1879
|
+
throw error;
|
|
1880
|
+
}
|
|
1881
|
+
})();
|
|
1882
|
+
done.catch(() => {});
|
|
1883
|
+
return {
|
|
1884
|
+
ok: true,
|
|
1885
|
+
output: done.then((r) => r.output),
|
|
1886
|
+
messages: done.then((r) => r.messages),
|
|
1887
|
+
usage: done.then((r) => r.usage),
|
|
1888
|
+
finishReason: done.then((r) => r.finishReason),
|
|
1889
|
+
fullStream: readable
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
const agent = {
|
|
1893
|
+
generate,
|
|
1894
|
+
stream,
|
|
1895
|
+
fn: () => generate
|
|
1896
|
+
};
|
|
1897
|
+
agent[RUNNABLE_META] = {
|
|
1898
|
+
name: config.name,
|
|
1899
|
+
inputSchema: config.input
|
|
1900
|
+
};
|
|
1901
|
+
return agent;
|
|
1902
|
+
}
|
|
1903
|
+
//#endregion
|
|
1904
|
+
//#region src/core/agents/flow/engine.ts
|
|
1905
|
+
/**
|
|
1906
|
+
* Wrap a hook callback so it can be passed to `fireHooks`.
|
|
1907
|
+
*
|
|
1908
|
+
* Generic over `TEvent` so the hook is called with the correct
|
|
1909
|
+
* event type without resorting to `any`.
|
|
1910
|
+
*/
|
|
1911
|
+
function createHookCaller(hook, event) {
|
|
1912
|
+
if (hook) return () => hook(event);
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Build a merged hook that runs engine and flow agent hooks sequentially.
|
|
1916
|
+
*
|
|
1917
|
+
* The `(event: never)` constraint is the widest function type under
|
|
1918
|
+
* strict mode — any single-argument function is assignable via
|
|
1919
|
+
* contravariance (`never extends T` for all `T`).
|
|
1920
|
+
*/
|
|
1921
|
+
function buildMergedHook(log, engineHook, flowHook) {
|
|
1922
|
+
if (!engineHook && !flowHook) return;
|
|
1923
|
+
const merged = async (event) => {
|
|
1924
|
+
await fireHooks(log, createHookCaller(engineHook, event), createHookCaller(flowHook, event));
|
|
1925
|
+
};
|
|
1926
|
+
return merged;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Built-in StepBuilder method names that custom steps must not shadow.
|
|
1930
|
+
*
|
|
1931
|
+
* @private
|
|
1932
|
+
*/
|
|
1933
|
+
const RESERVED_STEP_NAMES = new Set([
|
|
1934
|
+
"step",
|
|
1935
|
+
"agent",
|
|
1936
|
+
"map",
|
|
1937
|
+
"each",
|
|
1938
|
+
"reduce",
|
|
1939
|
+
"while",
|
|
1940
|
+
"all",
|
|
1941
|
+
"race"
|
|
1942
|
+
]);
|
|
1943
|
+
/**
|
|
1944
|
+
* Create a custom flow engine with additional step types
|
|
1945
|
+
* and/or default hooks.
|
|
1946
|
+
*
|
|
1947
|
+
* Returns a `flowAgent()`-like factory. Any custom steps defined
|
|
1948
|
+
* in `$` are merged into the handler's `$` parameter and fully typed.
|
|
1949
|
+
*
|
|
1950
|
+
* @typeParam TCustomSteps - The custom step definitions map.
|
|
1951
|
+
* @param config - Engine configuration including custom steps
|
|
1952
|
+
* and default hooks.
|
|
1953
|
+
* @returns A `FlowFactory` that creates flow agents with custom
|
|
1954
|
+
* `$` steps and engine-level default hooks.
|
|
1955
|
+
*
|
|
1956
|
+
* @example
|
|
1957
|
+
* ```typescript
|
|
1958
|
+
* const engine = createFlowEngine({
|
|
1959
|
+
* $: {
|
|
1960
|
+
* retry: async ({ ctx, config }) => {
|
|
1961
|
+
* let lastError: Error | undefined
|
|
1962
|
+
* for (let attempt = 0; attempt < config.attempts; attempt++) {
|
|
1963
|
+
* try {
|
|
1964
|
+
* return await config.execute({ attempt })
|
|
1965
|
+
* } catch (err) {
|
|
1966
|
+
* lastError = err as Error
|
|
1967
|
+
* }
|
|
1968
|
+
* }
|
|
1969
|
+
* throw lastError
|
|
1970
|
+
* },
|
|
1971
|
+
* },
|
|
1972
|
+
* onStart: ({ input }) => telemetry.trackStart(input),
|
|
1973
|
+
* })
|
|
1974
|
+
*
|
|
1975
|
+
* const myFlow = engine({
|
|
1976
|
+
* name: 'my-flow',
|
|
1977
|
+
* input: MyInput,
|
|
1978
|
+
* output: MyOutput,
|
|
1979
|
+
* }, async ({ input, $ }) => {
|
|
1980
|
+
* const data = await $.retry({
|
|
1981
|
+
* attempts: 3,
|
|
1982
|
+
* execute: async () => fetch('https://api.example.com/data'),
|
|
1983
|
+
* })
|
|
1984
|
+
* return data
|
|
1985
|
+
* })
|
|
1986
|
+
* ```
|
|
1987
|
+
*/
|
|
1988
|
+
function createFlowEngine(engineConfig) {
|
|
1989
|
+
for (const name of Object.keys(engineConfig.$ ?? {})) if (RESERVED_STEP_NAMES.has(name)) throw new Error(`Custom step "${name}" conflicts with a built-in StepBuilder method`);
|
|
1990
|
+
return function engineCreateFlowAgent(flowConfig, handler) {
|
|
1991
|
+
const hookLog = (flowConfig.logger ?? createDefaultLogger()).child({ source: "engine" });
|
|
1992
|
+
const { onStart: engineOnStart } = engineConfig;
|
|
1993
|
+
const { onStart: flowOnStart } = flowConfig;
|
|
1994
|
+
const { onFinish: engineOnFinish } = engineConfig;
|
|
1995
|
+
const { onFinish: flowOnFinish } = flowConfig;
|
|
1996
|
+
const { onError: engineOnError } = engineConfig;
|
|
1997
|
+
const { onError: flowOnError } = flowConfig;
|
|
1998
|
+
const { onStepStart: engineOnStepStart } = engineConfig;
|
|
1999
|
+
const { onStepStart: flowOnStepStart } = flowConfig;
|
|
2000
|
+
const { onStepFinish: engineOnStepFinish } = engineConfig;
|
|
2001
|
+
const { onStepFinish: flowOnStepFinish } = flowConfig;
|
|
2002
|
+
const mergedConfig = {
|
|
2003
|
+
...flowConfig,
|
|
2004
|
+
onStart: buildMergedHook(hookLog, engineOnStart, flowOnStart),
|
|
2005
|
+
onFinish: buildMergedHook(hookLog, engineOnFinish, flowOnFinish),
|
|
2006
|
+
onError: buildMergedHook(hookLog, engineOnError, flowOnError),
|
|
2007
|
+
onStepStart: buildMergedHook(hookLog, engineOnStepStart, flowOnStepStart),
|
|
2008
|
+
onStepFinish: buildMergedHook(hookLog, engineOnStepFinish, flowOnStepFinish)
|
|
2009
|
+
};
|
|
2010
|
+
const wrappedHandler = async (params) => {
|
|
2011
|
+
return handler({
|
|
2012
|
+
input: params.input,
|
|
2013
|
+
$: params.$,
|
|
2014
|
+
log: params.log
|
|
2015
|
+
});
|
|
2016
|
+
};
|
|
2017
|
+
return flowAgent(mergedConfig, wrappedHandler, { augment$: ($, ctx) => {
|
|
2018
|
+
const customSteps = {};
|
|
2019
|
+
for (const [name, factory] of Object.entries(engineConfig.$ ?? {})) customSteps[name] = (config) => factory({
|
|
2020
|
+
ctx: {
|
|
2021
|
+
signal: ctx.signal,
|
|
2022
|
+
log: ctx.log
|
|
2023
|
+
},
|
|
2024
|
+
config
|
|
2025
|
+
});
|
|
2026
|
+
return {
|
|
2027
|
+
...$,
|
|
2028
|
+
...customSteps
|
|
2029
|
+
};
|
|
2030
|
+
} });
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Supported OpenRouter models with pricing data.
|
|
2035
|
+
*/
|
|
2036
|
+
const MODELS = [...[
|
|
2037
|
+
{
|
|
2038
|
+
id: "openai/gpt-5.2-codex",
|
|
2039
|
+
category: "coding",
|
|
2040
|
+
pricing: {
|
|
2041
|
+
prompt: 175e-8,
|
|
2042
|
+
completion: 14e-6,
|
|
2043
|
+
inputCacheRead: 175e-9,
|
|
2044
|
+
webSearch: .01
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
id: "openai/gpt-5.2",
|
|
2049
|
+
category: "chat",
|
|
2050
|
+
pricing: {
|
|
2051
|
+
prompt: 175e-8,
|
|
2052
|
+
completion: 14e-6,
|
|
2053
|
+
inputCacheRead: 175e-9,
|
|
2054
|
+
webSearch: .01
|
|
2055
|
+
}
|
|
2056
|
+
},
|
|
2057
|
+
{
|
|
2058
|
+
id: "openai/gpt-5.1",
|
|
2059
|
+
category: "chat",
|
|
2060
|
+
pricing: {
|
|
2061
|
+
prompt: 125e-8,
|
|
2062
|
+
completion: 1e-5,
|
|
2063
|
+
inputCacheRead: 125e-9,
|
|
2064
|
+
webSearch: .01
|
|
2065
|
+
}
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
id: "openai/gpt-5",
|
|
2069
|
+
category: "chat",
|
|
2070
|
+
pricing: {
|
|
2071
|
+
prompt: 125e-8,
|
|
2072
|
+
completion: 1e-5,
|
|
2073
|
+
inputCacheRead: 125e-9,
|
|
2074
|
+
webSearch: .01
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
{
|
|
2078
|
+
id: "openai/gpt-5-mini",
|
|
2079
|
+
category: "chat",
|
|
2080
|
+
pricing: {
|
|
2081
|
+
prompt: 25e-8,
|
|
2082
|
+
completion: 2e-6,
|
|
2083
|
+
inputCacheRead: 25e-9,
|
|
2084
|
+
webSearch: .01
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
2087
|
+
{
|
|
2088
|
+
id: "openai/gpt-5-nano",
|
|
2089
|
+
category: "chat",
|
|
2090
|
+
pricing: {
|
|
2091
|
+
prompt: 5e-8,
|
|
2092
|
+
completion: 4e-7,
|
|
2093
|
+
inputCacheRead: 5e-9,
|
|
2094
|
+
webSearch: .01
|
|
2095
|
+
}
|
|
2096
|
+
},
|
|
2097
|
+
{
|
|
2098
|
+
id: "openai/gpt-4.1",
|
|
2099
|
+
category: "chat",
|
|
2100
|
+
pricing: {
|
|
2101
|
+
prompt: 2e-6,
|
|
2102
|
+
completion: 8e-6,
|
|
2103
|
+
inputCacheRead: 5e-7,
|
|
2104
|
+
webSearch: .01
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
id: "openai/gpt-4.1-mini",
|
|
2109
|
+
category: "chat",
|
|
2110
|
+
pricing: {
|
|
2111
|
+
prompt: 4e-7,
|
|
2112
|
+
completion: 16e-7,
|
|
2113
|
+
inputCacheRead: 1e-7,
|
|
2114
|
+
webSearch: .01
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
id: "openai/gpt-4.1-nano",
|
|
2119
|
+
category: "chat",
|
|
2120
|
+
pricing: {
|
|
2121
|
+
prompt: 1e-7,
|
|
2122
|
+
completion: 4e-7,
|
|
2123
|
+
inputCacheRead: 25e-9,
|
|
2124
|
+
webSearch: .01
|
|
2125
|
+
}
|
|
2126
|
+
},
|
|
2127
|
+
{
|
|
2128
|
+
id: "openai/gpt-4o",
|
|
2129
|
+
category: "chat",
|
|
2130
|
+
pricing: {
|
|
2131
|
+
prompt: 25e-7,
|
|
2132
|
+
completion: 1e-5,
|
|
2133
|
+
inputCacheRead: 125e-8
|
|
2134
|
+
}
|
|
2135
|
+
},
|
|
2136
|
+
{
|
|
2137
|
+
id: "openai/gpt-4o-mini",
|
|
2138
|
+
category: "chat",
|
|
2139
|
+
pricing: {
|
|
2140
|
+
prompt: 15e-8,
|
|
2141
|
+
completion: 6e-7,
|
|
2142
|
+
inputCacheRead: 75e-9
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
id: "openai/o3",
|
|
2147
|
+
category: "reasoning",
|
|
2148
|
+
pricing: {
|
|
2149
|
+
prompt: 2e-6,
|
|
2150
|
+
completion: 8e-6,
|
|
2151
|
+
inputCacheRead: 5e-7,
|
|
2152
|
+
webSearch: .01
|
|
2153
|
+
}
|
|
2154
|
+
},
|
|
2155
|
+
{
|
|
2156
|
+
id: "openai/o3-mini",
|
|
2157
|
+
category: "reasoning",
|
|
2158
|
+
pricing: {
|
|
2159
|
+
prompt: 11e-7,
|
|
2160
|
+
completion: 44e-7,
|
|
2161
|
+
inputCacheRead: 55e-8
|
|
2162
|
+
}
|
|
2163
|
+
},
|
|
2164
|
+
{
|
|
2165
|
+
id: "openai/o4-mini",
|
|
2166
|
+
category: "reasoning",
|
|
2167
|
+
pricing: {
|
|
2168
|
+
prompt: 11e-7,
|
|
2169
|
+
completion: 44e-7,
|
|
2170
|
+
inputCacheRead: 275e-9,
|
|
2171
|
+
webSearch: .01
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
]];
|
|
2175
|
+
/**
|
|
2176
|
+
* Look up a model definition by its identifier.
|
|
2177
|
+
*
|
|
2178
|
+
* @param id - The model identifier to look up.
|
|
2179
|
+
* @returns The matching model definition.
|
|
2180
|
+
* @throws {Error} If no model matches the given ID.
|
|
2181
|
+
*
|
|
2182
|
+
* @example
|
|
2183
|
+
* ```typescript
|
|
2184
|
+
* const m = model('openai/gpt-5.2-codex')
|
|
2185
|
+
* console.log(m.pricing.prompt) // 0.00000175
|
|
2186
|
+
* console.log(m.category) // 'coding'
|
|
2187
|
+
* ```
|
|
2188
|
+
*/
|
|
2189
|
+
function model(id) {
|
|
2190
|
+
const found = MODELS.find((m) => m.id === id);
|
|
2191
|
+
if (!found) throw new Error(`Unknown model: ${id}`);
|
|
2192
|
+
return found;
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Look up a model definition by its identifier, returning `undefined` if not found.
|
|
2196
|
+
*
|
|
2197
|
+
* Unlike {@link model}, this does not throw for unknown IDs — useful when
|
|
2198
|
+
* the caller can gracefully handle missing pricing (e.g. non-OpenRouter models).
|
|
2199
|
+
*
|
|
2200
|
+
* @param id - The model identifier to look up.
|
|
2201
|
+
* @returns The matching model definition, or `undefined`.
|
|
2202
|
+
*
|
|
2203
|
+
* @example
|
|
2204
|
+
* ```typescript
|
|
2205
|
+
* const m = tryModel('anthropic/claude-sonnet-4-20250514')
|
|
2206
|
+
* if (m) {
|
|
2207
|
+
* console.log(m.pricing.prompt)
|
|
2208
|
+
* }
|
|
2209
|
+
* ```
|
|
2210
|
+
*/
|
|
2211
|
+
function tryModel(id) {
|
|
2212
|
+
return MODELS.find((m) => m.id === id);
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Return supported model definitions, optionally filtered.
|
|
2216
|
+
*
|
|
2217
|
+
* @param filter - Optional predicate to filter models.
|
|
2218
|
+
* @returns A readonly array of matching model definitions.
|
|
2219
|
+
*
|
|
2220
|
+
* @example
|
|
2221
|
+
* ```typescript
|
|
2222
|
+
* const all = models()
|
|
2223
|
+
* const reasoning = models((m) => m.category === 'reasoning')
|
|
2224
|
+
* ```
|
|
2225
|
+
*/
|
|
2226
|
+
function models(filter) {
|
|
2227
|
+
return match(filter).with(P.nullish, () => MODELS).otherwise((fn) => MODELS.filter(fn));
|
|
2228
|
+
}
|
|
2229
|
+
//#endregion
|
|
2230
|
+
//#region src/utils/result.ts
|
|
2231
|
+
/**
|
|
2232
|
+
* Create a success `Result`.
|
|
2233
|
+
*
|
|
2234
|
+
* Spreads the payload flat onto the object alongside `ok: true`.
|
|
2235
|
+
*
|
|
2236
|
+
* @param value - The success payload.
|
|
2237
|
+
* @returns A success `Result<T>`.
|
|
2238
|
+
*
|
|
2239
|
+
* @example
|
|
2240
|
+
* ```typescript
|
|
2241
|
+
* return ok({ output: 'hello', messages: [] })
|
|
2242
|
+
* // → { ok: true, output: 'hello', messages: [] }
|
|
2243
|
+
* ```
|
|
2244
|
+
*/
|
|
2245
|
+
function ok(value) {
|
|
2246
|
+
return {
|
|
2247
|
+
...value,
|
|
2248
|
+
ok: true
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Create a failure `Result`.
|
|
2253
|
+
*
|
|
2254
|
+
* @param code - Machine-readable error code.
|
|
2255
|
+
* @param message - Human-readable error description.
|
|
2256
|
+
* @param cause - Optional original thrown error.
|
|
2257
|
+
* @returns A failure `Result` for any `T`.
|
|
2258
|
+
*
|
|
2259
|
+
* @example
|
|
2260
|
+
* ```typescript
|
|
2261
|
+
* return err('VALIDATION_ERROR', 'Name is required')
|
|
2262
|
+
* return err('AGENT_ERROR', error.message, error)
|
|
2263
|
+
* ```
|
|
2264
|
+
*/
|
|
2265
|
+
function err(code, message, cause) {
|
|
2266
|
+
return {
|
|
2267
|
+
ok: false,
|
|
2268
|
+
error: {
|
|
2269
|
+
code,
|
|
2270
|
+
message,
|
|
2271
|
+
cause
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Narrow a `Result<T>` to its success branch.
|
|
2277
|
+
*
|
|
2278
|
+
* @param result - The result to check.
|
|
2279
|
+
* @returns `true` when `result.ok` is `true`.
|
|
2280
|
+
*
|
|
2281
|
+
* @example
|
|
2282
|
+
* ```typescript
|
|
2283
|
+
* const result = await agent.generate('hello')
|
|
2284
|
+
* if (isOk(result)) {
|
|
2285
|
+
* console.log(result.output)
|
|
2286
|
+
* }
|
|
2287
|
+
* ```
|
|
2288
|
+
*/
|
|
2289
|
+
function isOk(result) {
|
|
2290
|
+
return result.ok;
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Narrow a `Result<T>` to its failure branch.
|
|
2294
|
+
*
|
|
2295
|
+
* @param result - The result to check.
|
|
2296
|
+
* @returns `true` when `result.ok` is `false`.
|
|
2297
|
+
*
|
|
2298
|
+
* @example
|
|
2299
|
+
* ```typescript
|
|
2300
|
+
* const result = await agent.generate('hello')
|
|
2301
|
+
* if (isErr(result)) {
|
|
2302
|
+
* console.error(result.error.code, result.error.message)
|
|
2303
|
+
* }
|
|
2304
|
+
* ```
|
|
2305
|
+
*/
|
|
2306
|
+
function isErr(result) {
|
|
2307
|
+
return !result.ok;
|
|
2308
|
+
}
|
|
2309
|
+
//#endregion
|
|
2310
|
+
export { agent, agentUsage, collectUsages, createDefaultLogger, createFlowEngine, createOpenRouter, createStepBuilder, err, flowAgent, flowAgentUsage, isErr, isOk, model, models, ok, openrouter, resolveOutput, safeStringify, safeStringifyJSON, sumTokenUsage, toError, tool, tryModel };
|
|
2311
|
+
|
|
2312
|
+
//# sourceMappingURL=index.mjs.map
|