@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
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { generateText, streamText, stepCountIs } from "ai";
|
|
2
|
+
import type { AsyncIterableStream } from "ai";
|
|
3
|
+
|
|
4
|
+
import { resolveOutput } from "@/core/agents/base/output.js";
|
|
5
|
+
import type {
|
|
6
|
+
Agent,
|
|
7
|
+
AgentConfig,
|
|
8
|
+
AgentOverrides,
|
|
9
|
+
GenerateResult,
|
|
10
|
+
Message,
|
|
11
|
+
StreamPart,
|
|
12
|
+
StreamResult,
|
|
13
|
+
SubAgents,
|
|
14
|
+
} from "@/core/agents/base/types.js";
|
|
15
|
+
import {
|
|
16
|
+
resolveModel,
|
|
17
|
+
buildAITools,
|
|
18
|
+
resolveSystem,
|
|
19
|
+
buildPrompt,
|
|
20
|
+
toTokenUsage,
|
|
21
|
+
} from "@/core/agents/base/utils.js";
|
|
22
|
+
import { createDefaultLogger } from "@/core/logger.js";
|
|
23
|
+
import type { Tool } from "@/core/tool.js";
|
|
24
|
+
import { fireHooks, wrapHook } from "@/lib/hooks.js";
|
|
25
|
+
import { withModelMiddleware } from "@/lib/middleware.js";
|
|
26
|
+
import { RUNNABLE_META, type RunnableMeta } from "@/lib/runnable.js";
|
|
27
|
+
import { toError } from "@/utils/error.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Safely read a property from `overrides`, which may be undefined.
|
|
31
|
+
* Replaces `overrides?.prop` optional chaining.
|
|
32
|
+
*
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
35
|
+
function readOverride<
|
|
36
|
+
TTools extends Record<string, Tool>,
|
|
37
|
+
TSubAgents extends SubAgents,
|
|
38
|
+
K extends keyof AgentOverrides<TTools, TSubAgents>,
|
|
39
|
+
>(
|
|
40
|
+
overrides: AgentOverrides<TTools, TSubAgents> | undefined,
|
|
41
|
+
key: K,
|
|
42
|
+
): AgentOverrides<TTools, TSubAgents>[K] | undefined {
|
|
43
|
+
if (overrides !== undefined) {
|
|
44
|
+
// eslint-disable-next-line security/detect-object-injection -- Key is a controlled function parameter, not user input
|
|
45
|
+
return overrides[key];
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Safely compute the JSON-serialized length of a value.
|
|
52
|
+
* Returns 0 if serialization fails (e.g. circular refs, BigInt).
|
|
53
|
+
*
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
function safeSerializedLength(value: unknown): number {
|
|
57
|
+
try {
|
|
58
|
+
const json = JSON.stringify(value);
|
|
59
|
+
return typeof json === "string" ? json.length : 0;
|
|
60
|
+
} catch {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return the value if the predicate is true, otherwise undefined.
|
|
67
|
+
* Replaces `predicate ? value : undefined` ternary.
|
|
68
|
+
*
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
function valueOrUndefined<T>(predicate: boolean, value: T): T | undefined {
|
|
72
|
+
if (predicate) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve an optional output param. Returns `resolveOutput(param)` if
|
|
80
|
+
* param is defined, otherwise undefined.
|
|
81
|
+
*
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
function resolveOptionalOutput(
|
|
85
|
+
param: import("@/core/agents/base/output.js").OutputParam | undefined,
|
|
86
|
+
): import("@/core/agents/base/output.js").OutputSpec | undefined {
|
|
87
|
+
if (param !== undefined) {
|
|
88
|
+
return resolveOutput(param);
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Safely extract a property from an object, returning `{}` if the
|
|
95
|
+
* property does not exist. Replaces `'key' in obj ? obj[key] : {}` ternary.
|
|
96
|
+
*
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
function extractProperty(obj: Record<string, unknown>, key: string): unknown {
|
|
100
|
+
if (key in obj) {
|
|
101
|
+
// eslint-disable-next-line security/detect-object-injection -- Key is a controlled function parameter, not user input
|
|
102
|
+
return obj[key];
|
|
103
|
+
}
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract token usage from a step's usage object, defaulting to 0
|
|
109
|
+
* when usage is undefined. Replaces optional chaining on `step.usage`.
|
|
110
|
+
*
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
function extractUsage(
|
|
114
|
+
usage: { inputTokens?: number; outputTokens?: number; totalTokens?: number } | undefined,
|
|
115
|
+
): {
|
|
116
|
+
inputTokens: number;
|
|
117
|
+
outputTokens: number;
|
|
118
|
+
totalTokens: number;
|
|
119
|
+
} {
|
|
120
|
+
if (usage !== undefined) {
|
|
121
|
+
const inputTokens = usage.inputTokens ?? 0;
|
|
122
|
+
const outputTokens = usage.outputTokens ?? 0;
|
|
123
|
+
return {
|
|
124
|
+
inputTokens,
|
|
125
|
+
outputTokens,
|
|
126
|
+
totalTokens: usage.totalTokens ?? inputTokens + outputTokens,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Return `ifOutput` when `output` is defined, `ifText` otherwise.
|
|
134
|
+
* Replaces `output ? aiResult.output : aiResult.text` ternary.
|
|
135
|
+
*
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
function pickByOutput<T>(output: unknown, ifOutput: T, ifText: T): T {
|
|
139
|
+
if (output !== undefined) {
|
|
140
|
+
return ifOutput;
|
|
141
|
+
}
|
|
142
|
+
return ifText;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create an agent with typed input, tools, subagents, and hooks.
|
|
147
|
+
*
|
|
148
|
+
* Agents run a tool loop (via the AI SDK's `generateText`) until a
|
|
149
|
+
* stop condition is met. They support:
|
|
150
|
+
* - **Typed input** via Zod schema + prompt template.
|
|
151
|
+
* - **Simple mode** — pass a string or messages directly.
|
|
152
|
+
* - **Tools** for function calling.
|
|
153
|
+
* - **Subagents** auto-wrapped as delegatable tools.
|
|
154
|
+
* - **Inline overrides** per call.
|
|
155
|
+
* - **Hooks** for observability.
|
|
156
|
+
* - **Result return type** that never throws.
|
|
157
|
+
*
|
|
158
|
+
* @typeParam TInput - Agent input type (default: `string | Message[]`).
|
|
159
|
+
* @typeParam TOutput - Agent output type (default: `string`).
|
|
160
|
+
* @typeParam TTools - Record of tools.
|
|
161
|
+
* @typeParam TSubAgents - Record of subagents.
|
|
162
|
+
* @param config - Agent configuration including name, model, schemas,
|
|
163
|
+
* tools, subagents, hooks, and logger.
|
|
164
|
+
* @returns An `Agent` instance with `.generate()`, `.stream()`, and `.fn()`.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* // Simple mode — pass a string directly
|
|
169
|
+
* const helper = agent({
|
|
170
|
+
* name: 'helper',
|
|
171
|
+
* model: 'openai/gpt-4.1',
|
|
172
|
+
* system: 'You are a helpful assistant.',
|
|
173
|
+
* })
|
|
174
|
+
* await helper.generate('What is TypeScript?')
|
|
175
|
+
*
|
|
176
|
+
* // Typed mode — input schema + prompt template
|
|
177
|
+
* const summarizer = agent({
|
|
178
|
+
* name: 'summarizer',
|
|
179
|
+
* input: z.object({ text: z.string() }),
|
|
180
|
+
* model: 'openai/gpt-4.1',
|
|
181
|
+
* prompt: ({ input }) => `Summarize:\n\n${input.text}`,
|
|
182
|
+
* })
|
|
183
|
+
* await summarizer.generate({ text: '...' })
|
|
184
|
+
*
|
|
185
|
+
* // Export as a plain function
|
|
186
|
+
* export const summarize = summarizer.fn()
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export function agent<
|
|
190
|
+
TInput = string | Message[],
|
|
191
|
+
TOutput = string,
|
|
192
|
+
TTools extends Record<string, Tool> = {},
|
|
193
|
+
TSubAgents extends SubAgents = {},
|
|
194
|
+
>(
|
|
195
|
+
config: AgentConfig<TInput, TOutput, TTools, TSubAgents>,
|
|
196
|
+
): Agent<TInput, TOutput, TTools, TSubAgents> {
|
|
197
|
+
const baseLogger = config.logger ?? createDefaultLogger();
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validate raw input against the config schema, if present.
|
|
201
|
+
*
|
|
202
|
+
* Returns a discriminated union: `{ ok: true, input }` on success,
|
|
203
|
+
* `{ ok: false, error }` when validation fails.
|
|
204
|
+
*
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
function validateInput(
|
|
208
|
+
rawInput: TInput,
|
|
209
|
+
): { ok: true; input: TInput } | { ok: false; error: { code: string; message: string } } {
|
|
210
|
+
if (!config.input) {
|
|
211
|
+
return { ok: true, input: rawInput };
|
|
212
|
+
}
|
|
213
|
+
const parsed = config.input.safeParse(rawInput);
|
|
214
|
+
if (!parsed.success) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
error: {
|
|
218
|
+
code: "VALIDATION_ERROR",
|
|
219
|
+
message: `Input validation failed: ${parsed.error.message}`,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return { ok: true, input: parsed.data as TInput };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function generate(
|
|
227
|
+
rawInput: TInput,
|
|
228
|
+
overrides?: AgentOverrides<TTools, TSubAgents>,
|
|
229
|
+
): Promise<import("@/utils/result.js").Result<GenerateResult<TOutput>>> {
|
|
230
|
+
const validated = validateInput(rawInput);
|
|
231
|
+
if (!validated.ok) {
|
|
232
|
+
return { ok: false, error: validated.error };
|
|
233
|
+
}
|
|
234
|
+
const input = validated.input;
|
|
235
|
+
|
|
236
|
+
const overrideLogger = readOverride(overrides, "logger");
|
|
237
|
+
const log = (overrideLogger ?? baseLogger).child({ agentId: config.name });
|
|
238
|
+
const startedAt = Date.now();
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const overrideModel = readOverride(overrides, "model");
|
|
242
|
+
const modelRef = overrideModel ?? config.model;
|
|
243
|
+
const baseModel = resolveModel(modelRef);
|
|
244
|
+
const model = await withModelMiddleware({ model: baseModel });
|
|
245
|
+
|
|
246
|
+
const overrideTools = readOverride(overrides, "tools");
|
|
247
|
+
const overrideAgents = readOverride(overrides, "agents");
|
|
248
|
+
const mergedTools = { ...config.tools, ...overrideTools } as Record<string, Tool>;
|
|
249
|
+
const mergedAgents = { ...config.agents, ...overrideAgents } as SubAgents;
|
|
250
|
+
const hasTools = Object.keys(mergedTools).length > 0;
|
|
251
|
+
const hasAgents = Object.keys(mergedAgents).length > 0;
|
|
252
|
+
|
|
253
|
+
const aiTools = buildAITools(
|
|
254
|
+
valueOrUndefined(hasTools, mergedTools),
|
|
255
|
+
valueOrUndefined(hasAgents, mergedAgents),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const overrideSystem = readOverride(overrides, "system");
|
|
259
|
+
const systemConfig = overrideSystem ?? config.system;
|
|
260
|
+
const system = resolveSystem(systemConfig, input);
|
|
261
|
+
|
|
262
|
+
const promptParams = buildPrompt(input, config);
|
|
263
|
+
|
|
264
|
+
const overrideOutput = readOverride(overrides, "output");
|
|
265
|
+
const outputParam = overrideOutput ?? config.output;
|
|
266
|
+
const output = resolveOptionalOutput(outputParam);
|
|
267
|
+
|
|
268
|
+
await fireHooks(
|
|
269
|
+
log,
|
|
270
|
+
wrapHook(config.onStart, { input }),
|
|
271
|
+
wrapHook(readOverride(overrides, "onStart"), { input }),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
log.debug("agent.generate start", { name: config.name });
|
|
275
|
+
|
|
276
|
+
const overrideMaxSteps = readOverride(overrides, "maxSteps");
|
|
277
|
+
const maxSteps = overrideMaxSteps ?? config.maxSteps ?? 20;
|
|
278
|
+
const overrideSignal = readOverride(overrides, "signal");
|
|
279
|
+
const stepCounter = { value: 0 };
|
|
280
|
+
const aiResult = await generateText({
|
|
281
|
+
model,
|
|
282
|
+
system,
|
|
283
|
+
...promptParams,
|
|
284
|
+
tools: aiTools,
|
|
285
|
+
output,
|
|
286
|
+
stopWhen: stepCountIs(maxSteps),
|
|
287
|
+
abortSignal: overrideSignal,
|
|
288
|
+
onStepFinish: async (step) => {
|
|
289
|
+
const stepId = `${config.name}:${stepCounter.value++}`;
|
|
290
|
+
const toolCalls = (step.toolCalls ?? []).map((tc) => {
|
|
291
|
+
const args = extractProperty(tc, "args");
|
|
292
|
+
return { toolName: tc.toolName, argsTextLength: safeSerializedLength(args) };
|
|
293
|
+
});
|
|
294
|
+
const toolResults = (step.toolResults ?? []).map((tr) => {
|
|
295
|
+
const result = extractProperty(tr, "result");
|
|
296
|
+
return { toolName: tr.toolName, resultTextLength: safeSerializedLength(result) };
|
|
297
|
+
});
|
|
298
|
+
const usage = extractUsage(step.usage);
|
|
299
|
+
const event = { stepId, toolCalls, toolResults, usage };
|
|
300
|
+
await fireHooks(
|
|
301
|
+
log,
|
|
302
|
+
wrapHook(config.onStepFinish, event),
|
|
303
|
+
wrapHook(readOverride(overrides, "onStepFinish"), event),
|
|
304
|
+
);
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const duration = Date.now() - startedAt;
|
|
309
|
+
|
|
310
|
+
const generateResult: GenerateResult<TOutput> = {
|
|
311
|
+
output: pickByOutput(output, aiResult.output, aiResult.text) as TOutput,
|
|
312
|
+
messages: aiResult.response.messages as Message[],
|
|
313
|
+
usage: toTokenUsage(aiResult.totalUsage),
|
|
314
|
+
finishReason: aiResult.finishReason,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
await fireHooks(
|
|
318
|
+
log,
|
|
319
|
+
wrapHook(config.onFinish, { input, result: generateResult, duration }),
|
|
320
|
+
wrapHook(readOverride(overrides, "onFinish"), {
|
|
321
|
+
input,
|
|
322
|
+
result: generateResult as GenerateResult,
|
|
323
|
+
duration,
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
log.debug("agent.generate finish", { name: config.name, duration });
|
|
328
|
+
|
|
329
|
+
return { ok: true, ...generateResult };
|
|
330
|
+
} catch (thrown) {
|
|
331
|
+
const error = toError(thrown);
|
|
332
|
+
const duration = Date.now() - startedAt;
|
|
333
|
+
|
|
334
|
+
log.error("agent.generate error", { name: config.name, error: error.message, duration });
|
|
335
|
+
|
|
336
|
+
await fireHooks(
|
|
337
|
+
log,
|
|
338
|
+
wrapHook(config.onError, { input, error }),
|
|
339
|
+
wrapHook(readOverride(overrides, "onError"), { input, error }),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
ok: false,
|
|
344
|
+
error: {
|
|
345
|
+
code: "AGENT_ERROR",
|
|
346
|
+
message: error.message,
|
|
347
|
+
cause: error,
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function stream(
|
|
354
|
+
rawInput: TInput,
|
|
355
|
+
overrides?: AgentOverrides<TTools, TSubAgents>,
|
|
356
|
+
): Promise<import("@/utils/result.js").Result<StreamResult<TOutput>>> {
|
|
357
|
+
const validated = validateInput(rawInput);
|
|
358
|
+
if (!validated.ok) {
|
|
359
|
+
return { ok: false, error: validated.error };
|
|
360
|
+
}
|
|
361
|
+
const input = validated.input;
|
|
362
|
+
|
|
363
|
+
const overrideLogger = readOverride(overrides, "logger");
|
|
364
|
+
const log = (overrideLogger ?? baseLogger).child({ agentId: config.name });
|
|
365
|
+
const startedAt = Date.now();
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const overrideModel = readOverride(overrides, "model");
|
|
369
|
+
const modelRef = overrideModel ?? config.model;
|
|
370
|
+
const baseModel = resolveModel(modelRef);
|
|
371
|
+
const model = await withModelMiddleware({ model: baseModel });
|
|
372
|
+
|
|
373
|
+
const overrideTools = readOverride(overrides, "tools");
|
|
374
|
+
const overrideAgents = readOverride(overrides, "agents");
|
|
375
|
+
const mergedTools = { ...config.tools, ...overrideTools } as Record<string, Tool>;
|
|
376
|
+
const mergedAgents = { ...config.agents, ...overrideAgents } as SubAgents;
|
|
377
|
+
const hasTools = Object.keys(mergedTools).length > 0;
|
|
378
|
+
const hasAgents = Object.keys(mergedAgents).length > 0;
|
|
379
|
+
|
|
380
|
+
const aiTools = buildAITools(
|
|
381
|
+
valueOrUndefined(hasTools, mergedTools),
|
|
382
|
+
valueOrUndefined(hasAgents, mergedAgents),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const overrideSystem = readOverride(overrides, "system");
|
|
386
|
+
const systemConfig = overrideSystem ?? config.system;
|
|
387
|
+
const system = resolveSystem(systemConfig, input);
|
|
388
|
+
|
|
389
|
+
const promptParams = buildPrompt(input, config);
|
|
390
|
+
|
|
391
|
+
const overrideOutput = readOverride(overrides, "output");
|
|
392
|
+
const outputParam = overrideOutput ?? config.output;
|
|
393
|
+
const output = resolveOptionalOutput(outputParam);
|
|
394
|
+
|
|
395
|
+
await fireHooks(
|
|
396
|
+
log,
|
|
397
|
+
wrapHook(config.onStart, { input }),
|
|
398
|
+
wrapHook(readOverride(overrides, "onStart"), { input }),
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
log.debug("agent.stream start", { name: config.name });
|
|
402
|
+
|
|
403
|
+
const overrideMaxSteps = readOverride(overrides, "maxSteps");
|
|
404
|
+
const maxSteps = overrideMaxSteps ?? config.maxSteps ?? 20;
|
|
405
|
+
const overrideSignal = readOverride(overrides, "signal");
|
|
406
|
+
const stepCounter = { value: 0 };
|
|
407
|
+
const aiResult = streamText({
|
|
408
|
+
model,
|
|
409
|
+
system,
|
|
410
|
+
...promptParams,
|
|
411
|
+
tools: aiTools,
|
|
412
|
+
output,
|
|
413
|
+
stopWhen: stepCountIs(maxSteps),
|
|
414
|
+
abortSignal: overrideSignal,
|
|
415
|
+
onStepFinish: async (step) => {
|
|
416
|
+
const stepId = `${config.name}:${stepCounter.value++}`;
|
|
417
|
+
const toolCalls = (step.toolCalls ?? []).map((tc) => {
|
|
418
|
+
const args = extractProperty(tc, "args");
|
|
419
|
+
return { toolName: tc.toolName, argsTextLength: safeSerializedLength(args) };
|
|
420
|
+
});
|
|
421
|
+
const toolResults = (step.toolResults ?? []).map((tr) => {
|
|
422
|
+
const result = extractProperty(tr, "result");
|
|
423
|
+
return { toolName: tr.toolName, resultTextLength: safeSerializedLength(result) };
|
|
424
|
+
});
|
|
425
|
+
const usage = extractUsage(step.usage);
|
|
426
|
+
const event = { stepId, toolCalls, toolResults, usage };
|
|
427
|
+
await fireHooks(
|
|
428
|
+
log,
|
|
429
|
+
wrapHook(config.onStepFinish, event),
|
|
430
|
+
wrapHook(readOverride(overrides, "onStepFinish"), event),
|
|
431
|
+
);
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const { readable, writable } = new TransformStream<StreamPart, StreamPart>();
|
|
436
|
+
|
|
437
|
+
const done = (async () => {
|
|
438
|
+
const writer = writable.getWriter();
|
|
439
|
+
try {
|
|
440
|
+
for await (const part of aiResult.fullStream) {
|
|
441
|
+
await writer.write(part as StreamPart);
|
|
442
|
+
}
|
|
443
|
+
await writer.close();
|
|
444
|
+
} catch (error) {
|
|
445
|
+
await writer.abort(error).catch(() => {});
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const finalOutput = pickByOutput(
|
|
450
|
+
output,
|
|
451
|
+
await aiResult.output,
|
|
452
|
+
await aiResult.text,
|
|
453
|
+
) as TOutput;
|
|
454
|
+
const response = await aiResult.response;
|
|
455
|
+
const finalMessages = response.messages as Message[];
|
|
456
|
+
const finalUsage = toTokenUsage(await aiResult.totalUsage);
|
|
457
|
+
const finalFinishReason = await aiResult.finishReason;
|
|
458
|
+
|
|
459
|
+
const duration = Date.now() - startedAt;
|
|
460
|
+
|
|
461
|
+
const generateResult: GenerateResult<TOutput> = {
|
|
462
|
+
output: finalOutput,
|
|
463
|
+
messages: finalMessages,
|
|
464
|
+
usage: finalUsage,
|
|
465
|
+
finishReason: finalFinishReason,
|
|
466
|
+
};
|
|
467
|
+
await fireHooks(
|
|
468
|
+
log,
|
|
469
|
+
wrapHook(config.onFinish, { input, result: generateResult, duration }),
|
|
470
|
+
wrapHook(readOverride(overrides, "onFinish"), {
|
|
471
|
+
input,
|
|
472
|
+
result: generateResult as GenerateResult,
|
|
473
|
+
duration,
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
log.debug("agent.stream finish", { name: config.name, duration });
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
output: finalOutput,
|
|
481
|
+
messages: finalMessages,
|
|
482
|
+
usage: finalUsage,
|
|
483
|
+
finishReason: finalFinishReason,
|
|
484
|
+
};
|
|
485
|
+
})();
|
|
486
|
+
|
|
487
|
+
// Catch stream errors: fire onError hooks and prevent unhandled rejections
|
|
488
|
+
done.catch(async (thrown) => {
|
|
489
|
+
const error = toError(thrown);
|
|
490
|
+
const duration = Date.now() - startedAt;
|
|
491
|
+
|
|
492
|
+
log.error("agent.stream error", { name: config.name, error: error.message, duration });
|
|
493
|
+
|
|
494
|
+
await fireHooks(
|
|
495
|
+
log,
|
|
496
|
+
wrapHook(config.onError, { input, error }),
|
|
497
|
+
wrapHook(readOverride(overrides, "onError"), { input, error }),
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const streamResult: StreamResult<TOutput> = {
|
|
502
|
+
output: done.then((r) => r.output),
|
|
503
|
+
messages: done.then((r) => r.messages),
|
|
504
|
+
usage: done.then((r) => r.usage),
|
|
505
|
+
finishReason: done.then((r) => r.finishReason),
|
|
506
|
+
fullStream: readable as AsyncIterableStream<StreamPart>,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
return { ok: true, ...streamResult };
|
|
510
|
+
} catch (thrown) {
|
|
511
|
+
const error = toError(thrown);
|
|
512
|
+
const duration = Date.now() - startedAt;
|
|
513
|
+
|
|
514
|
+
log.error("agent.stream error", { name: config.name, error: error.message, duration });
|
|
515
|
+
|
|
516
|
+
await fireHooks(
|
|
517
|
+
log,
|
|
518
|
+
wrapHook(config.onError, { input, error }),
|
|
519
|
+
wrapHook(readOverride(overrides, "onError"), { input, error }),
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
ok: false,
|
|
524
|
+
error: {
|
|
525
|
+
code: "AGENT_ERROR",
|
|
526
|
+
message: error.message,
|
|
527
|
+
cause: error,
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// eslint-disable-next-line no-shadow -- Local variable is the return value constructed inside its own factory function
|
|
534
|
+
const agent: Agent<TInput, TOutput, TTools, TSubAgents> = {
|
|
535
|
+
generate,
|
|
536
|
+
stream,
|
|
537
|
+
fn: () => generate,
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// eslint-disable-next-line security/detect-object-injection -- Symbol-keyed property access; symbols cannot be user-controlled
|
|
541
|
+
(agent as unknown as Record<symbol, unknown>)[RUNNABLE_META] = {
|
|
542
|
+
name: config.name,
|
|
543
|
+
inputSchema: config.input,
|
|
544
|
+
} satisfies RunnableMeta;
|
|
545
|
+
|
|
546
|
+
return agent;
|
|
547
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Output } from "ai";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { resolveOutput } from "@/core/agents/base/output.js";
|
|
6
|
+
|
|
7
|
+
const mockIsZodArray = vi.hoisted(() =>
|
|
8
|
+
vi.fn<(...args: unknown[]) => boolean>((...args: unknown[]) => {
|
|
9
|
+
const { z: zod } = require("zod");
|
|
10
|
+
// Default: use real JSON schema check
|
|
11
|
+
try {
|
|
12
|
+
return zod.toJSONSchema(args[0]).type === "array";
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
vi.mock("@/utils/zod.js", async (importOriginal) => {
|
|
20
|
+
const original = await importOriginal<typeof import("@/utils/zod.js")>();
|
|
21
|
+
return {
|
|
22
|
+
...original,
|
|
23
|
+
isZodArray: (...args: unknown[]) => mockIsZodArray(...args),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("resolveOutput", () => {
|
|
28
|
+
it("passes through Output.text()", () => {
|
|
29
|
+
const text = Output.text();
|
|
30
|
+
expect(resolveOutput(text)).toBe(text);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("passes through Output.object()", () => {
|
|
34
|
+
const obj = Output.object({ schema: z.object({ x: z.number() }) });
|
|
35
|
+
expect(resolveOutput(obj)).toBe(obj);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("passes through Output.array()", () => {
|
|
39
|
+
const arr = Output.array({ element: z.object({ x: z.number() }) });
|
|
40
|
+
expect(resolveOutput(arr)).toBe(arr);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("passes through Output.choice()", () => {
|
|
44
|
+
const choice = Output.choice({ options: ["a", "b"] as const });
|
|
45
|
+
expect(resolveOutput(choice)).toBe(choice);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("passes through Output.json()", () => {
|
|
49
|
+
const json = Output.json();
|
|
50
|
+
expect(resolveOutput(json)).toBe(json);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("wraps a Zod object schema in Output.object()", () => {
|
|
54
|
+
const schema = z.object({ name: z.string() });
|
|
55
|
+
const resolved = resolveOutput(schema);
|
|
56
|
+
|
|
57
|
+
expect(resolved).not.toBe(schema);
|
|
58
|
+
expect(resolved).toHaveProperty("parseCompleteOutput");
|
|
59
|
+
expect(resolved).toHaveProperty("parsePartialOutput");
|
|
60
|
+
expect(resolved.name).toBe("object");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("wraps a Zod array schema in Output.array()", () => {
|
|
64
|
+
const schema = z.array(z.object({ name: z.string() }));
|
|
65
|
+
const resolved = resolveOutput(schema);
|
|
66
|
+
|
|
67
|
+
expect(resolved).not.toBe(schema);
|
|
68
|
+
expect(resolved).toHaveProperty("parseCompleteOutput");
|
|
69
|
+
expect(resolved).toHaveProperty("parsePartialOutput");
|
|
70
|
+
expect(resolved.name).toBe("array");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("wraps a non-array Zod schema in Output.object()", () => {
|
|
74
|
+
const schema = z.string();
|
|
75
|
+
const resolved = resolveOutput(schema);
|
|
76
|
+
|
|
77
|
+
expect(resolved).not.toBe(schema);
|
|
78
|
+
expect(resolved).toHaveProperty("parseCompleteOutput");
|
|
79
|
+
expect(resolved.name).toBe("object");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("throws when Zod array element schema cannot be extracted", () => {
|
|
83
|
+
// Force isZodArray to return true for our fake object
|
|
84
|
+
mockIsZodArray.mockReturnValueOnce(true);
|
|
85
|
+
|
|
86
|
+
// Create an object that looks like a Zod array but has no extractable element
|
|
87
|
+
const fakeArraySchema = { _zod: { def: {} } };
|
|
88
|
+
|
|
89
|
+
expect(() => resolveOutput(fakeArraySchema as never)).toThrow(
|
|
90
|
+
"Failed to extract element schema from Zod array",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Output } from "ai";
|
|
2
|
+
import { match } from "ts-pattern";
|
|
3
|
+
import type { ZodType } from "zod";
|
|
4
|
+
|
|
5
|
+
import { isZodArray } from "@/utils/zod.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base constraint for AI SDK output strategies.
|
|
9
|
+
*
|
|
10
|
+
* Reaches through the `Output` namespace to the underlying
|
|
11
|
+
* `Output<OUTPUT, PARTIAL, ELEMENT>` interface. Use this as the
|
|
12
|
+
* type for any field that accepts `Output.text()`, `Output.object()`, etc.
|
|
13
|
+
*/
|
|
14
|
+
export type OutputSpec = Output.Output<unknown, unknown>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Accepted values for the `output` config field.
|
|
18
|
+
*
|
|
19
|
+
* Allows either:
|
|
20
|
+
* - An AI SDK `Output` strategy (`Output.text()`, `Output.object()`, etc.)
|
|
21
|
+
* - A raw Zod schema — automatically wrapped in `Output.object()` or
|
|
22
|
+
* `Output.array()` depending on the schema type.
|
|
23
|
+
*/
|
|
24
|
+
export type OutputParam = OutputSpec | ZodType;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve an `OutputParam` into an `OutputSpec`.
|
|
28
|
+
*
|
|
29
|
+
* If the value is already an `OutputSpec`, it is returned as-is.
|
|
30
|
+
* If it is a raw Zod schema:
|
|
31
|
+
* - `z.array(...)` → `Output.array({ element: innerSchema })`
|
|
32
|
+
* - Anything else → `Output.object({ schema })`
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export function resolveOutput(output: OutputParam): OutputSpec {
|
|
37
|
+
// OutputSpec instances have `parseCompleteOutput` — Zod schemas don't
|
|
38
|
+
return match("parseCompleteOutput" in output)
|
|
39
|
+
.with(true, () => output as OutputSpec)
|
|
40
|
+
.otherwise(() => {
|
|
41
|
+
const schema = output as ZodType;
|
|
42
|
+
return match(isZodArray(schema))
|
|
43
|
+
.with(true, () => {
|
|
44
|
+
const def = (schema as unknown as Record<string, unknown>)._zod as
|
|
45
|
+
| { def: { element?: ZodType } }
|
|
46
|
+
| undefined;
|
|
47
|
+
if (def != null && def.def.element != null) {
|
|
48
|
+
return Output.array({ element: def.def.element });
|
|
49
|
+
}
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Failed to extract element schema from Zod array. " +
|
|
52
|
+
"Pass Output.array({ element: elementSchema }) explicitly.",
|
|
53
|
+
);
|
|
54
|
+
})
|
|
55
|
+
.otherwise(() => Output.object({ schema }));
|
|
56
|
+
});
|
|
57
|
+
}
|