@codemation/core-nodes 0.4.3 → 1.0.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/CHANGELOG.md +44 -0
- package/dist/index.cjs +480 -410
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +220 -116
- package/dist/index.d.ts +220 -116
- package/dist/index.js +476 -403
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/chatModels/OpenAIChatModelFactory.ts +17 -8
- package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +123 -0
- package/src/index.ts +1 -1
- package/src/nodes/AIAgentExecutionHelpersFactory.ts +45 -59
- package/src/nodes/AIAgentNode.ts +293 -288
- package/src/nodes/AgentMessageFactory.ts +57 -49
- package/src/nodes/AgentStructuredOutputRunner.ts +65 -71
- package/src/nodes/AgentToolExecutionCoordinator.ts +3 -14
- package/src/nodes/aiAgentSupport.types.ts +7 -2
- package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +0 -46
package/src/nodes/AIAgentNode.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentGuardrailConfig,
|
|
3
3
|
AgentToolCall,
|
|
4
|
+
ChatLanguageModel,
|
|
5
|
+
ChatLanguageModelCallOptions,
|
|
4
6
|
ChatModelConfig,
|
|
5
7
|
ChatModelFactory,
|
|
6
8
|
Item,
|
|
7
9
|
Items,
|
|
8
10
|
JsonValue,
|
|
9
|
-
LangChainChatModelLike,
|
|
10
11
|
NodeExecutionContext,
|
|
11
12
|
NodeInputsByPort,
|
|
12
13
|
RunnableNode,
|
|
13
14
|
RunnableNodeExecuteArgs,
|
|
14
15
|
Tool,
|
|
15
16
|
ToolConfig,
|
|
16
|
-
LangChainStructuredOutputModelLike,
|
|
17
17
|
ZodSchemaAny,
|
|
18
18
|
} from "@codemation/core";
|
|
19
19
|
|
|
@@ -34,7 +34,15 @@ import {
|
|
|
34
34
|
type NodeResolver,
|
|
35
35
|
} from "@codemation/core";
|
|
36
36
|
|
|
37
|
-
import {
|
|
37
|
+
import type { AssistantModelMessage, GenerateTextResult, LanguageModel, ModelMessage, ToolSet } from "ai";
|
|
38
|
+
import { Output, generateText, jsonSchema } from "ai";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* OUTPUT generic must extend AI SDK's `Output<OUTPUT, PARTIAL, ELEMENT>` which is parametric on
|
|
42
|
+
* `any`; there is no narrower concrete type we can substitute that accepts both text-only and
|
|
43
|
+
* structured turns uniformly.
|
|
44
|
+
*/
|
|
45
|
+
type AnyGenerateTextResult = GenerateTextResult<ToolSet, any>;
|
|
38
46
|
import { z } from "zod";
|
|
39
47
|
|
|
40
48
|
import type { AIAgent } from "./AIAgentConfig";
|
|
@@ -60,25 +68,35 @@ type ResolvedGuardrails = Required<Pick<AgentGuardrailConfig, "maxTurns" | "onTu
|
|
|
60
68
|
/** Everything needed to run the agent loop for one item (shared across items in the same activation). */
|
|
61
69
|
interface PreparedAgentExecution {
|
|
62
70
|
readonly ctx: NodeExecutionContext<AIAgent<any, any>>;
|
|
63
|
-
readonly model:
|
|
71
|
+
readonly model: ChatLanguageModel;
|
|
64
72
|
readonly resolvedTools: ReadonlyArray<ResolvedTool>;
|
|
65
73
|
readonly guardrails: ResolvedGuardrails;
|
|
66
74
|
readonly languageModelConnectionNodeId: string;
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
/** Result of one `generateText` turn with tools disabled for auto-execution. */
|
|
78
|
+
interface TurnResult {
|
|
79
|
+
readonly assistantMessage: AssistantModelMessage | undefined;
|
|
80
|
+
readonly text: string;
|
|
81
|
+
readonly toolCalls: ReadonlyArray<AgentToolCall>;
|
|
82
|
+
readonly usage: ModelUsage;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface ModelUsage {
|
|
86
|
+
readonly inputTokens?: number;
|
|
87
|
+
readonly outputTokens?: number;
|
|
88
|
+
readonly totalTokens?: number;
|
|
89
|
+
readonly cachedInputTokens?: number;
|
|
90
|
+
readonly reasoningTokens?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
@node({ packageName: "@codemation/core-nodes" })
|
|
70
94
|
export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
71
95
|
kind = "node" as const;
|
|
72
96
|
outputPorts = ["main"] as const;
|
|
73
|
-
/**
|
|
74
|
-
* Engine validates {@link RunnableNodeConfig.inputSchema} (Zod) on {@code item.json} before enqueue, then resolves
|
|
75
|
-
* per-item **`itemExpr`** leaves on config before {@link #execute}. Prefer modeling prompts as
|
|
76
|
-
* {@code { messages: [{ role, content }, ...] }} (on input or config) so persisted inputs are visible in the UI.
|
|
77
|
-
*/
|
|
78
97
|
readonly inputSchema = z.unknown();
|
|
79
98
|
|
|
80
99
|
private readonly connectionCredentialExecutionContextFactory: ConnectionCredentialExecutionContextFactory;
|
|
81
|
-
/** One resolved model/tools bundle per activation context (same ctx across items in a batch). */
|
|
82
100
|
private readonly preparedByExecutionContext = new WeakMap<
|
|
83
101
|
NodeExecutionContext<AIAgent<any, any>>,
|
|
84
102
|
Promise<PreparedAgentExecution>
|
|
@@ -123,9 +141,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
123
141
|
}
|
|
124
142
|
}
|
|
125
143
|
|
|
126
|
-
/**
|
|
127
|
-
* Resolves the chat model and tools once per activation, then reuses for every item in the batch.
|
|
128
|
-
*/
|
|
129
144
|
private async prepareExecution(ctx: NodeExecutionContext<AIAgent<any, any>>): Promise<PreparedAgentExecution> {
|
|
130
145
|
const chatModelFactory = this.nodeResolver.resolve(ctx.config.chatModel.type) as ChatModelFactory<ChatModelConfig>;
|
|
131
146
|
const languageModelCredentialContext = this.connectionCredentialExecutionContextFactory.forConnectionNode(ctx, {
|
|
@@ -144,9 +159,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
144
159
|
};
|
|
145
160
|
}
|
|
146
161
|
|
|
147
|
-
/**
|
|
148
|
-
* One item: build prompts, optionally bind tools, run the multi-turn loop, map the final model message to workflow JSON.
|
|
149
|
-
*/
|
|
150
162
|
private async runAgentForItem(
|
|
151
163
|
prepared: PreparedAgentExecution,
|
|
152
164
|
item: Item,
|
|
@@ -156,7 +168,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
156
168
|
const { ctx } = prepared;
|
|
157
169
|
const itemInputsByPort = AgentItemPortMap.fromItem(item);
|
|
158
170
|
const itemScopedTools = this.createItemScopedTools(prepared.resolvedTools, ctx, item, itemIndex, items);
|
|
159
|
-
const conversation:
|
|
171
|
+
const conversation: ModelMessage[] = [...this.createPromptMessages(item, itemIndex, items, ctx)];
|
|
160
172
|
if (ctx.config.outputSchema && itemScopedTools.length === 0) {
|
|
161
173
|
const structuredOutput = await this.structuredOutputRunner.resolve({
|
|
162
174
|
model: prepared.model,
|
|
@@ -165,36 +177,19 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
165
177
|
conversation,
|
|
166
178
|
agentName: this.getAgentDisplayName(ctx),
|
|
167
179
|
nodeId: ctx.nodeId,
|
|
168
|
-
invokeTextModel: async (messages) =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
prepared.languageModelConnectionNodeId,
|
|
172
|
-
messages,
|
|
173
|
-
ctx,
|
|
174
|
-
itemInputsByPort,
|
|
175
|
-
prepared.guardrails.modelInvocationOptions,
|
|
176
|
-
),
|
|
177
|
-
invokeStructuredModel: async (structuredModel, messages) =>
|
|
178
|
-
await this.invokeStructuredModel(
|
|
179
|
-
structuredModel,
|
|
180
|
-
prepared.languageModelConnectionNodeId,
|
|
181
|
-
messages,
|
|
182
|
-
ctx,
|
|
183
|
-
itemInputsByPort,
|
|
184
|
-
prepared.guardrails.modelInvocationOptions,
|
|
185
|
-
),
|
|
180
|
+
invokeTextModel: async (messages) => await this.invokeTextTurn(prepared, itemInputsByPort, messages, []),
|
|
181
|
+
invokeStructuredModel: async (schema, messages, structuredOptions) =>
|
|
182
|
+
await this.invokeStructuredTurn(prepared, itemInputsByPort, schema, messages, structuredOptions),
|
|
186
183
|
});
|
|
187
184
|
await ctx.telemetry.recordMetric({ name: CodemationTelemetryMetricNames.agentTurns, value: 1 });
|
|
188
185
|
await ctx.telemetry.recordMetric({ name: CodemationTelemetryMetricNames.agentToolCalls, value: 0 });
|
|
189
186
|
return this.buildOutputItem(item, structuredOutput);
|
|
190
187
|
}
|
|
191
|
-
const modelWithTools = this.bindToolsToModel(prepared.model, itemScopedTools);
|
|
192
188
|
const loopResult = await this.runTurnLoopUntilFinalAnswer({
|
|
193
189
|
prepared,
|
|
194
190
|
itemInputsByPort,
|
|
195
191
|
itemScopedTools,
|
|
196
192
|
conversation,
|
|
197
|
-
modelWithTools,
|
|
198
193
|
});
|
|
199
194
|
await ctx.telemetry.recordMetric({ name: CodemationTelemetryMetricNames.agentTurns, value: loopResult.turnCount });
|
|
200
195
|
await ctx.telemetry.recordMetric({
|
|
@@ -205,44 +200,40 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
205
200
|
prepared,
|
|
206
201
|
itemInputsByPort,
|
|
207
202
|
conversation,
|
|
208
|
-
loopResult.
|
|
203
|
+
loopResult.finalText,
|
|
209
204
|
itemScopedTools.length > 0,
|
|
210
205
|
);
|
|
211
206
|
return this.buildOutputItem(item, outputJson);
|
|
212
207
|
}
|
|
213
208
|
|
|
214
209
|
/**
|
|
215
|
-
*
|
|
210
|
+
* Multi-turn loop:
|
|
211
|
+
* - Each turn is a single `generateText` call with tools exposed but **not auto-executed**
|
|
212
|
+
* (we control tool dispatch so that {@link AgentToolExecutionCoordinator} drives repair /
|
|
213
|
+
* connection-invocation recording / transient-error handling exactly like before).
|
|
214
|
+
* - When the model returns no tool calls the loop ends with the model's text as the final answer.
|
|
215
|
+
* - Respects `guardrails.maxTurns` and `guardrails.onTurnLimitReached`.
|
|
216
216
|
*/
|
|
217
217
|
private async runTurnLoopUntilFinalAnswer(args: {
|
|
218
218
|
prepared: PreparedAgentExecution;
|
|
219
219
|
itemInputsByPort: NodeInputsByPort;
|
|
220
220
|
itemScopedTools: ReadonlyArray<ItemScopedToolBinding>;
|
|
221
|
-
conversation:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const {
|
|
225
|
-
const { ctx, guardrails, languageModelConnectionNodeId } = prepared;
|
|
221
|
+
conversation: ModelMessage[];
|
|
222
|
+
}): Promise<Readonly<{ finalText: string; turnCount: number; toolCallCount: number }>> {
|
|
223
|
+
const { prepared, itemInputsByPort, itemScopedTools, conversation } = args;
|
|
224
|
+
const { ctx, guardrails } = prepared;
|
|
226
225
|
|
|
227
|
-
let
|
|
226
|
+
let finalText = "";
|
|
228
227
|
let toolCallCount = 0;
|
|
229
228
|
let turnCount = 0;
|
|
230
229
|
const repairAttemptsByToolName = new Map<string, number>();
|
|
231
230
|
|
|
232
231
|
for (let turn = 1; turn <= guardrails.maxTurns; turn++) {
|
|
233
232
|
turnCount = turn;
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
languageModelConnectionNodeId,
|
|
237
|
-
conversation,
|
|
238
|
-
ctx,
|
|
239
|
-
itemInputsByPort,
|
|
240
|
-
guardrails.modelInvocationOptions,
|
|
241
|
-
);
|
|
242
|
-
finalResponse = response;
|
|
233
|
+
const result = await this.invokeTextTurn(prepared, itemInputsByPort, conversation, itemScopedTools);
|
|
234
|
+
finalText = result.text;
|
|
243
235
|
|
|
244
|
-
|
|
245
|
-
if (toolCalls.length === 0) {
|
|
236
|
+
if (result.toolCalls.length === 0) {
|
|
246
237
|
break;
|
|
247
238
|
}
|
|
248
239
|
|
|
@@ -251,7 +242,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
251
242
|
break;
|
|
252
243
|
}
|
|
253
244
|
|
|
254
|
-
const plannedToolCalls = this.planToolCalls(itemScopedTools, toolCalls, ctx.nodeId);
|
|
245
|
+
const plannedToolCalls = this.planToolCalls(itemScopedTools, result.toolCalls, ctx.nodeId);
|
|
255
246
|
toolCallCount += plannedToolCalls.length;
|
|
256
247
|
await this.markQueuedTools(plannedToolCalls, ctx);
|
|
257
248
|
const executedToolCalls = await this.toolExecutionCoordinator.execute({
|
|
@@ -260,14 +251,17 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
260
251
|
agentName: this.getAgentDisplayName(ctx),
|
|
261
252
|
repairAttemptsByToolName,
|
|
262
253
|
});
|
|
263
|
-
this.appendAssistantAndToolMessages(
|
|
254
|
+
this.appendAssistantAndToolMessages(
|
|
255
|
+
conversation,
|
|
256
|
+
result.assistantMessage,
|
|
257
|
+
result.text,
|
|
258
|
+
result.toolCalls,
|
|
259
|
+
executedToolCalls,
|
|
260
|
+
);
|
|
264
261
|
}
|
|
265
262
|
|
|
266
|
-
if (!finalResponse) {
|
|
267
|
-
throw new Error(`AIAgent "${ctx.config.name ?? ctx.nodeId}" did not produce a model response.`);
|
|
268
|
-
}
|
|
269
263
|
return {
|
|
270
|
-
|
|
264
|
+
finalText,
|
|
271
265
|
turnCount,
|
|
272
266
|
toolCallCount,
|
|
273
267
|
};
|
|
@@ -290,54 +284,42 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
290
284
|
}
|
|
291
285
|
|
|
292
286
|
private appendAssistantAndToolMessages(
|
|
293
|
-
conversation:
|
|
294
|
-
assistantMessage:
|
|
287
|
+
conversation: ModelMessage[],
|
|
288
|
+
assistantMessage: AssistantModelMessage | undefined,
|
|
289
|
+
text: string,
|
|
290
|
+
toolCalls: ReadonlyArray<AgentToolCall>,
|
|
295
291
|
executedToolCalls: ReadonlyArray<ExecutedToolCall>,
|
|
296
292
|
): void {
|
|
297
293
|
conversation.push(
|
|
298
|
-
assistantMessage,
|
|
299
|
-
|
|
300
|
-
AgentMessageFactory.createToolMessage(toolCall.toolCallId, toolCall.serialized),
|
|
301
|
-
),
|
|
294
|
+
assistantMessage ?? AgentMessageFactory.createAssistantWithToolCalls(text, toolCalls),
|
|
295
|
+
AgentMessageFactory.createToolResultsMessage(executedToolCalls),
|
|
302
296
|
);
|
|
303
297
|
}
|
|
304
298
|
|
|
305
299
|
private async resolveFinalOutputJson(
|
|
306
300
|
prepared: PreparedAgentExecution,
|
|
307
301
|
itemInputsByPort: NodeInputsByPort,
|
|
308
|
-
conversation: ReadonlyArray<
|
|
309
|
-
|
|
302
|
+
conversation: ReadonlyArray<ModelMessage>,
|
|
303
|
+
finalText: string,
|
|
310
304
|
wasToolEnabledRun: boolean,
|
|
311
305
|
): Promise<unknown> {
|
|
312
306
|
if (!prepared.ctx.config.outputSchema) {
|
|
313
|
-
return AgentOutputFactory.fromAgentContent(
|
|
307
|
+
return AgentOutputFactory.fromAgentContent(finalText);
|
|
314
308
|
}
|
|
309
|
+
const conversationWithFinal: ReadonlyArray<ModelMessage> = wasToolEnabledRun
|
|
310
|
+
? [...conversation, { role: "assistant", content: finalText }]
|
|
311
|
+
: conversation;
|
|
315
312
|
return await this.structuredOutputRunner.resolve({
|
|
316
313
|
model: prepared.model,
|
|
317
314
|
chatModelConfig: prepared.ctx.config.chatModel,
|
|
318
315
|
schema: prepared.ctx.config.outputSchema,
|
|
319
|
-
conversation:
|
|
320
|
-
|
|
316
|
+
conversation: conversationWithFinal,
|
|
317
|
+
rawFinalText: finalText,
|
|
321
318
|
agentName: this.getAgentDisplayName(prepared.ctx),
|
|
322
319
|
nodeId: prepared.ctx.nodeId,
|
|
323
|
-
invokeTextModel: async (messages) =>
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
prepared.languageModelConnectionNodeId,
|
|
327
|
-
messages,
|
|
328
|
-
prepared.ctx,
|
|
329
|
-
itemInputsByPort,
|
|
330
|
-
prepared.guardrails.modelInvocationOptions,
|
|
331
|
-
),
|
|
332
|
-
invokeStructuredModel: async (structuredModel, messages) =>
|
|
333
|
-
await this.invokeStructuredModel(
|
|
334
|
-
structuredModel,
|
|
335
|
-
prepared.languageModelConnectionNodeId,
|
|
336
|
-
messages,
|
|
337
|
-
prepared.ctx,
|
|
338
|
-
itemInputsByPort,
|
|
339
|
-
prepared.guardrails.modelInvocationOptions,
|
|
340
|
-
),
|
|
320
|
+
invokeTextModel: async (messages) => await this.invokeTextTurn(prepared, itemInputsByPort, messages, []),
|
|
321
|
+
invokeStructuredModel: async (schema, messages, structuredOptions) =>
|
|
322
|
+
await this.invokeStructuredTurn(prepared, itemInputsByPort, schema, messages, structuredOptions),
|
|
341
323
|
});
|
|
342
324
|
}
|
|
343
325
|
|
|
@@ -345,16 +327,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
345
327
|
return AgentOutputFactory.replaceJson(item, outputJson);
|
|
346
328
|
}
|
|
347
329
|
|
|
348
|
-
private bindToolsToModel(
|
|
349
|
-
model: LangChainChatModelLike,
|
|
350
|
-
itemScopedTools: ReadonlyArray<ItemScopedToolBinding>,
|
|
351
|
-
): LangChainChatModelLike {
|
|
352
|
-
if (itemScopedTools.length === 0 || !model.bindTools) {
|
|
353
|
-
return model;
|
|
354
|
-
}
|
|
355
|
-
return model.bindTools(itemScopedTools.map((entry) => entry.langChainTool));
|
|
356
|
-
}
|
|
357
|
-
|
|
358
330
|
private resolveTools(toolConfigs: ReadonlyArray<ToolConfig>): ReadonlyArray<ResolvedTool> {
|
|
359
331
|
const resolvedTools = toolConfigs.map((config) => ({
|
|
360
332
|
config,
|
|
@@ -381,69 +353,117 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
381
353
|
connectionNodeId: ConnectionNodeIdFactory.toolConnectionNodeId(ctx.nodeId, entry.config.name),
|
|
382
354
|
getCredentialRequirements: () => entry.config.getCredentialRequirements?.() ?? [],
|
|
383
355
|
});
|
|
384
|
-
|
|
385
|
-
entry,
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
356
|
+
return {
|
|
357
|
+
config: entry.config,
|
|
358
|
+
inputSchema: entry.runtime.inputSchema,
|
|
359
|
+
execute: async (input: unknown): Promise<unknown> => {
|
|
360
|
+
const validated = entry.runtime.inputSchema.parse(input) as unknown;
|
|
361
|
+
return await entry.runtime.execute({
|
|
362
|
+
config: entry.config,
|
|
363
|
+
input: validated,
|
|
364
|
+
ctx: toolCredentialContext,
|
|
365
|
+
item,
|
|
366
|
+
itemIndex,
|
|
367
|
+
items,
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
} satisfies ItemScopedToolBinding;
|
|
393
371
|
});
|
|
394
372
|
}
|
|
395
373
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Builds an AI SDK {@link ToolSet} where every tool ships a pre-converted JSON Schema (via
|
|
376
|
+
* {@link jsonSchema}) — not the raw Zod schema — and carries **no** `execute`. Two reasons:
|
|
377
|
+
*
|
|
378
|
+
* 1. Codemation owns tool dispatch + the per-tool repair loop (see {@link AgentToolExecutionCoordinator}),
|
|
379
|
+
* so the AI SDK must surface tool calls back to us instead of auto-running them.
|
|
380
|
+
* 2. The AI SDK's `asSchema` helper discriminates between Zod v3 / Zod v4 / Standard Schema via
|
|
381
|
+
* runtime feature-detection (`~standard`, `_zod`, etc.). Handing it a pre-built
|
|
382
|
+
* {@link jsonSchema} record — which is tagged with `Symbol.for('vercel.ai.schema')` — skips all
|
|
383
|
+
* of that detection and guarantees the provider receives a draft-07 JSON Schema with
|
|
384
|
+
* `additionalProperties: false` at every object depth (see {@link OpenAiStrictJsonSchemaFactory}
|
|
385
|
+
* for the same logic applied to structured-output schemas). Codemation still runs its own Zod
|
|
386
|
+
* validation on tool inputs before execute — the schema handed to the model is advisory.
|
|
387
|
+
*/
|
|
388
|
+
private buildToolSet(itemScopedTools: ReadonlyArray<ItemScopedToolBinding>): ToolSet | undefined {
|
|
389
|
+
if (itemScopedTools.length === 0) return undefined;
|
|
390
|
+
const toolSet: Record<string, { description?: string; inputSchema: ReturnType<typeof jsonSchema> }> = {};
|
|
391
|
+
for (const entry of itemScopedTools) {
|
|
392
|
+
const schemaRecord = this.executionHelpers.createJsonSchemaRecord(entry.inputSchema, {
|
|
393
|
+
schemaName: entry.config.name,
|
|
394
|
+
requireObjectRoot: true,
|
|
395
|
+
});
|
|
396
|
+
toolSet[entry.config.name] = {
|
|
397
|
+
description: entry.config.description,
|
|
398
|
+
inputSchema: jsonSchema(schemaRecord as Parameters<typeof jsonSchema>[0]),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return toolSet as unknown as ToolSet;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* One `generateText` turn (no auto tool execution) with Codemation-owned child-span telemetry
|
|
406
|
+
* and connection-invocation state recording.
|
|
407
|
+
*/
|
|
408
|
+
private async invokeTextTurn(
|
|
409
|
+
prepared: PreparedAgentExecution,
|
|
410
|
+
itemInputsByPort: NodeInputsByPort,
|
|
411
|
+
messages: ReadonlyArray<ModelMessage>,
|
|
412
|
+
itemScopedTools: ReadonlyArray<ItemScopedToolBinding>,
|
|
413
|
+
): Promise<TurnResult> {
|
|
404
414
|
const invocationId = ConnectionInvocationIdFactory.create();
|
|
405
415
|
const startedAt = new Date();
|
|
406
416
|
const summarizedInput = this.summarizeLlmMessages(messages);
|
|
417
|
+
const { ctx, model, languageModelConnectionNodeId, guardrails } = prepared;
|
|
407
418
|
const span = this.createModelInvocationSpan(ctx, invocationId, startedAt);
|
|
408
|
-
await ctx.nodeState?.markQueued({
|
|
409
|
-
|
|
419
|
+
await ctx.nodeState?.markQueued({
|
|
420
|
+
nodeId: languageModelConnectionNodeId,
|
|
421
|
+
activationId: ctx.activationId,
|
|
422
|
+
inputsByPort: itemInputsByPort,
|
|
423
|
+
});
|
|
424
|
+
await ctx.nodeState?.markRunning({
|
|
425
|
+
nodeId: languageModelConnectionNodeId,
|
|
426
|
+
activationId: ctx.activationId,
|
|
427
|
+
inputsByPort: itemInputsByPort,
|
|
428
|
+
});
|
|
410
429
|
try {
|
|
411
|
-
const
|
|
430
|
+
const tools = this.buildToolSet(itemScopedTools);
|
|
431
|
+
const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
|
|
432
|
+
const result = await generateText({
|
|
433
|
+
model: model.languageModel as LanguageModel,
|
|
434
|
+
messages: [...messages],
|
|
435
|
+
tools,
|
|
436
|
+
toolChoice: tools ? "auto" : undefined,
|
|
437
|
+
maxOutputTokens: callOptions.maxOutputTokens,
|
|
438
|
+
temperature: callOptions.temperature,
|
|
439
|
+
providerOptions: callOptions.providerOptions as Record<string, Record<string, never>>,
|
|
440
|
+
maxRetries: 0,
|
|
441
|
+
});
|
|
442
|
+
const turnResult = this.extractTurnResult(result as AnyGenerateTextResult);
|
|
412
443
|
const finishedAt = new Date();
|
|
413
444
|
await ctx.nodeState?.markCompleted({
|
|
414
|
-
nodeId,
|
|
445
|
+
nodeId: languageModelConnectionNodeId,
|
|
415
446
|
activationId: ctx.activationId,
|
|
416
|
-
inputsByPort,
|
|
417
|
-
outputs: AgentOutputFactory.fromUnknown({
|
|
418
|
-
content: AgentMessageFactory.extractContent(response),
|
|
419
|
-
}),
|
|
447
|
+
inputsByPort: itemInputsByPort,
|
|
448
|
+
outputs: AgentOutputFactory.fromUnknown({ content: turnResult.text }),
|
|
420
449
|
});
|
|
421
|
-
|
|
422
|
-
await span.attachArtifact({
|
|
423
|
-
|
|
424
|
-
contentType: "application/json",
|
|
425
|
-
previewJson: summarizedInput,
|
|
426
|
-
});
|
|
427
|
-
await span.attachArtifact({
|
|
428
|
-
kind: "ai.response",
|
|
429
|
-
contentType: "application/json",
|
|
430
|
-
previewJson: content,
|
|
431
|
-
});
|
|
432
|
-
await this.recordModelUsageMetrics(span, response, ctx);
|
|
450
|
+
await span.attachArtifact({ kind: "ai.messages", contentType: "application/json", previewJson: summarizedInput });
|
|
451
|
+
await span.attachArtifact({ kind: "ai.response", contentType: "application/json", previewJson: turnResult.text });
|
|
452
|
+
await this.recordModelUsageMetrics(span, turnResult.usage, ctx);
|
|
433
453
|
await span.end({ status: "ok", endedAt: finishedAt });
|
|
434
454
|
await ctx.nodeState?.appendConnectionInvocation({
|
|
435
455
|
invocationId,
|
|
436
|
-
connectionNodeId:
|
|
456
|
+
connectionNodeId: languageModelConnectionNodeId,
|
|
437
457
|
parentAgentNodeId: ctx.nodeId,
|
|
438
458
|
parentAgentActivationId: ctx.activationId,
|
|
439
459
|
status: "completed",
|
|
440
460
|
managedInput: summarizedInput,
|
|
441
|
-
managedOutput:
|
|
461
|
+
managedOutput: turnResult.text,
|
|
442
462
|
queuedAt: startedAt.toISOString(),
|
|
443
463
|
startedAt: startedAt.toISOString(),
|
|
444
464
|
finishedAt: finishedAt.toISOString(),
|
|
445
465
|
});
|
|
446
|
-
return
|
|
466
|
+
return turnResult;
|
|
447
467
|
} catch (error) {
|
|
448
468
|
await span.end({
|
|
449
469
|
status: "error",
|
|
@@ -454,62 +474,85 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
454
474
|
error,
|
|
455
475
|
invocationId,
|
|
456
476
|
startedAt,
|
|
457
|
-
nodeId,
|
|
477
|
+
nodeId: languageModelConnectionNodeId,
|
|
458
478
|
ctx,
|
|
459
|
-
inputsByPort,
|
|
460
|
-
managedInput:
|
|
479
|
+
inputsByPort: itemInputsByPort,
|
|
480
|
+
managedInput: summarizedInput,
|
|
461
481
|
});
|
|
462
482
|
}
|
|
463
483
|
}
|
|
464
484
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
485
|
+
/**
|
|
486
|
+
* Structured-output turn: runs `generateText({ output: Output.object({ schema }) })` via the
|
|
487
|
+
* structured-output runner. We keep this as a separate helper because the runner needs the raw
|
|
488
|
+
* validated value (not just text) back, and must be able to retry on Zod failures.
|
|
489
|
+
*/
|
|
490
|
+
private async invokeStructuredTurn(
|
|
491
|
+
prepared: PreparedAgentExecution,
|
|
492
|
+
itemInputsByPort: NodeInputsByPort,
|
|
493
|
+
schema: ZodSchemaAny | Readonly<Record<string, unknown>>,
|
|
494
|
+
messages: ReadonlyArray<ModelMessage>,
|
|
495
|
+
structuredOptions: { readonly strict?: boolean; readonly schemaName?: string } | undefined,
|
|
472
496
|
): Promise<unknown> {
|
|
473
497
|
const invocationId = ConnectionInvocationIdFactory.create();
|
|
474
498
|
const startedAt = new Date();
|
|
475
499
|
const summarizedInput = this.summarizeLlmMessages(messages);
|
|
500
|
+
const { ctx, model, languageModelConnectionNodeId, guardrails } = prepared;
|
|
476
501
|
const span = this.createModelInvocationSpan(ctx, invocationId, startedAt);
|
|
477
|
-
await ctx.nodeState?.markQueued({
|
|
478
|
-
|
|
502
|
+
await ctx.nodeState?.markQueued({
|
|
503
|
+
nodeId: languageModelConnectionNodeId,
|
|
504
|
+
activationId: ctx.activationId,
|
|
505
|
+
inputsByPort: itemInputsByPort,
|
|
506
|
+
});
|
|
507
|
+
await ctx.nodeState?.markRunning({
|
|
508
|
+
nodeId: languageModelConnectionNodeId,
|
|
509
|
+
activationId: ctx.activationId,
|
|
510
|
+
inputsByPort: itemInputsByPort,
|
|
511
|
+
});
|
|
479
512
|
try {
|
|
480
|
-
const
|
|
513
|
+
const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
|
|
514
|
+
const outputSchema =
|
|
515
|
+
structuredOptions?.strict && !this.isZodSchema(schema)
|
|
516
|
+
? Output.object({ schema: jsonSchema(schema as Parameters<typeof jsonSchema>[0]) as never })
|
|
517
|
+
: Output.object({ schema: schema as ZodSchemaAny });
|
|
518
|
+
const result = await generateText({
|
|
519
|
+
model: model.languageModel as LanguageModel,
|
|
520
|
+
messages: [...messages],
|
|
521
|
+
experimental_output: outputSchema,
|
|
522
|
+
maxOutputTokens: callOptions.maxOutputTokens,
|
|
523
|
+
temperature: callOptions.temperature,
|
|
524
|
+
providerOptions: callOptions.providerOptions as Record<string, Record<string, never>>,
|
|
525
|
+
maxRetries: 0,
|
|
526
|
+
});
|
|
527
|
+
const turnResult = this.extractTurnResult(result as AnyGenerateTextResult);
|
|
481
528
|
const finishedAt = new Date();
|
|
482
529
|
await ctx.nodeState?.markCompleted({
|
|
483
|
-
nodeId,
|
|
530
|
+
nodeId: languageModelConnectionNodeId,
|
|
484
531
|
activationId: ctx.activationId,
|
|
485
|
-
inputsByPort,
|
|
486
|
-
outputs: AgentOutputFactory.fromUnknown(
|
|
487
|
-
});
|
|
488
|
-
await span.attachArtifact({
|
|
489
|
-
kind: "ai.messages",
|
|
490
|
-
contentType: "application/json",
|
|
491
|
-
previewJson: summarizedInput,
|
|
532
|
+
inputsByPort: itemInputsByPort,
|
|
533
|
+
outputs: AgentOutputFactory.fromUnknown(result.experimental_output),
|
|
492
534
|
});
|
|
535
|
+
await span.attachArtifact({ kind: "ai.messages", contentType: "application/json", previewJson: summarizedInput });
|
|
493
536
|
await span.attachArtifact({
|
|
494
537
|
kind: "ai.response.structured",
|
|
495
538
|
contentType: "application/json",
|
|
496
|
-
previewJson: this.resultToJsonValue(
|
|
539
|
+
previewJson: this.resultToJsonValue(result.experimental_output),
|
|
497
540
|
});
|
|
498
|
-
await this.recordModelUsageMetrics(span,
|
|
541
|
+
await this.recordModelUsageMetrics(span, turnResult.usage, ctx);
|
|
499
542
|
await span.end({ status: "ok", endedAt: finishedAt });
|
|
500
543
|
await ctx.nodeState?.appendConnectionInvocation({
|
|
501
544
|
invocationId,
|
|
502
|
-
connectionNodeId:
|
|
545
|
+
connectionNodeId: languageModelConnectionNodeId,
|
|
503
546
|
parentAgentNodeId: ctx.nodeId,
|
|
504
547
|
parentAgentActivationId: ctx.activationId,
|
|
505
548
|
status: "completed",
|
|
506
549
|
managedInput: summarizedInput,
|
|
507
|
-
managedOutput: this.resultToJsonValue(
|
|
550
|
+
managedOutput: this.resultToJsonValue(result.experimental_output),
|
|
508
551
|
queuedAt: startedAt.toISOString(),
|
|
509
552
|
startedAt: startedAt.toISOString(),
|
|
510
553
|
finishedAt: finishedAt.toISOString(),
|
|
511
554
|
});
|
|
512
|
-
return
|
|
555
|
+
return result.experimental_output;
|
|
513
556
|
} catch (error) {
|
|
514
557
|
await span.end({
|
|
515
558
|
status: "error",
|
|
@@ -520,14 +563,72 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
520
563
|
error,
|
|
521
564
|
invocationId,
|
|
522
565
|
startedAt,
|
|
523
|
-
nodeId,
|
|
566
|
+
nodeId: languageModelConnectionNodeId,
|
|
524
567
|
ctx,
|
|
525
|
-
inputsByPort,
|
|
526
|
-
managedInput:
|
|
568
|
+
inputsByPort: itemInputsByPort,
|
|
569
|
+
managedInput: summarizedInput,
|
|
527
570
|
});
|
|
528
571
|
}
|
|
529
572
|
}
|
|
530
573
|
|
|
574
|
+
private isZodSchema(schema: ZodSchemaAny | Readonly<Record<string, unknown>>): schema is ZodSchemaAny {
|
|
575
|
+
const candidate = schema as { parse?: unknown };
|
|
576
|
+
return typeof candidate.parse === "function";
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private resolveCallOptions(
|
|
580
|
+
model: ChatLanguageModel,
|
|
581
|
+
overrides: AgentGuardrailConfig["modelInvocationOptions"] | undefined,
|
|
582
|
+
): ChatLanguageModelCallOptions {
|
|
583
|
+
const defaults = model.defaultCallOptions ?? {};
|
|
584
|
+
return {
|
|
585
|
+
maxOutputTokens: overrides?.maxTokens ?? defaults.maxOutputTokens,
|
|
586
|
+
temperature: defaults.temperature,
|
|
587
|
+
providerOptions: (overrides?.providerOptions ??
|
|
588
|
+
defaults.providerOptions) as ChatLanguageModelCallOptions["providerOptions"],
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private extractTurnResult(result: AnyGenerateTextResult): TurnResult {
|
|
593
|
+
const usage = this.extractUsageFromResult(result);
|
|
594
|
+
const text = result.text;
|
|
595
|
+
const toolCalls: ReadonlyArray<AgentToolCall> = result.toolCalls.map((toolCall) => ({
|
|
596
|
+
id: toolCall.toolCallId,
|
|
597
|
+
name: toolCall.toolName,
|
|
598
|
+
input: (toolCall as { input?: unknown }).input,
|
|
599
|
+
}));
|
|
600
|
+
const assistantMessage = this.extractAssistantMessage(result);
|
|
601
|
+
return {
|
|
602
|
+
assistantMessage,
|
|
603
|
+
text,
|
|
604
|
+
toolCalls,
|
|
605
|
+
usage,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private extractAssistantMessage(result: AnyGenerateTextResult): AssistantModelMessage | undefined {
|
|
610
|
+
const responseMessages: ReadonlyArray<ModelMessage> = result.response.messages;
|
|
611
|
+
const assistantMessages = responseMessages.filter((m) => m.role === "assistant");
|
|
612
|
+
return assistantMessages[assistantMessages.length - 1];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private extractUsageFromResult(result: AnyGenerateTextResult): ModelUsage {
|
|
616
|
+
const usage = result.usage;
|
|
617
|
+
const inputTokens = this.toFiniteNumber(usage.inputTokens);
|
|
618
|
+
const outputTokens = this.toFiniteNumber(usage.outputTokens);
|
|
619
|
+
const totalTokens =
|
|
620
|
+
this.toFiniteNumber(usage.totalTokens) ??
|
|
621
|
+
(inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
|
|
622
|
+
const cachedInputTokens = this.toFiniteNumber(usage.cachedInputTokens);
|
|
623
|
+
const reasoningTokens = this.toFiniteNumber(usage.reasoningTokens);
|
|
624
|
+
return { inputTokens, outputTokens, totalTokens, cachedInputTokens, reasoningTokens };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private toFiniteNumber(value: unknown): number | undefined {
|
|
628
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
629
|
+
return value;
|
|
630
|
+
}
|
|
631
|
+
|
|
531
632
|
private createModelInvocationSpan(
|
|
532
633
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
533
634
|
invocationId: string,
|
|
@@ -547,14 +648,18 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
547
648
|
|
|
548
649
|
private async recordModelUsageMetrics(
|
|
549
650
|
span: ReturnType<NodeExecutionContext["telemetry"]["startChildSpan"]>,
|
|
550
|
-
|
|
651
|
+
usage: ModelUsage,
|
|
551
652
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
552
653
|
) {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
654
|
+
const entries: ReadonlyArray<readonly [string, number | undefined]> = [
|
|
655
|
+
[GenAiTelemetryAttributeNames.usageInputTokens, usage.inputTokens],
|
|
656
|
+
[GenAiTelemetryAttributeNames.usageOutputTokens, usage.outputTokens],
|
|
657
|
+
[GenAiTelemetryAttributeNames.usageTotalTokens, usage.totalTokens],
|
|
658
|
+
[GenAiTelemetryAttributeNames.usageCacheReadInputTokens, usage.cachedInputTokens],
|
|
659
|
+
[GenAiTelemetryAttributeNames.usageReasoningTokens, usage.reasoningTokens],
|
|
660
|
+
];
|
|
661
|
+
for (const [name, value] of entries) {
|
|
662
|
+
if (value === undefined) continue;
|
|
558
663
|
await span.recordMetric({ name, value });
|
|
559
664
|
}
|
|
560
665
|
await this.captureCostTrackingUsage(span, ctx, usage);
|
|
@@ -563,38 +668,32 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
563
668
|
private async captureCostTrackingUsage(
|
|
564
669
|
span: ReturnType<NodeExecutionContext["telemetry"]["startChildSpan"]>,
|
|
565
670
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
566
|
-
usage:
|
|
671
|
+
usage: ModelUsage,
|
|
567
672
|
): Promise<void> {
|
|
568
673
|
const costTracking = span.costTracking;
|
|
569
|
-
if (!costTracking)
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
674
|
+
if (!costTracking) return;
|
|
572
675
|
const provider = ctx.config.chatModel.provider;
|
|
573
676
|
const pricingKey = ctx.config.chatModel.modelName;
|
|
574
|
-
if (!provider || !pricingKey)
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
const inputTokens = usage[GenAiTelemetryAttributeNames.usageInputTokens];
|
|
578
|
-
const outputTokens = usage[GenAiTelemetryAttributeNames.usageOutputTokens];
|
|
579
|
-
if (inputTokens !== undefined) {
|
|
677
|
+
if (!provider || !pricingKey) return;
|
|
678
|
+
if (usage.inputTokens !== undefined) {
|
|
580
679
|
await costTracking.captureUsage({
|
|
581
680
|
component: "chat",
|
|
582
681
|
provider,
|
|
583
682
|
operation: "completion.input",
|
|
584
683
|
pricingKey,
|
|
585
684
|
usageUnit: "input_tokens",
|
|
586
|
-
quantity: inputTokens,
|
|
685
|
+
quantity: usage.inputTokens,
|
|
587
686
|
modelName: pricingKey,
|
|
588
687
|
});
|
|
589
688
|
}
|
|
590
|
-
if (outputTokens !== undefined) {
|
|
689
|
+
if (usage.outputTokens !== undefined) {
|
|
591
690
|
await costTracking.captureUsage({
|
|
592
691
|
component: "chat",
|
|
593
692
|
provider,
|
|
594
693
|
operation: "completion.output",
|
|
595
694
|
pricingKey,
|
|
596
695
|
usageUnit: "output_tokens",
|
|
597
|
-
quantity: outputTokens,
|
|
696
|
+
quantity: usage.outputTokens,
|
|
598
697
|
modelName: pricingKey,
|
|
599
698
|
});
|
|
600
699
|
}
|
|
@@ -604,90 +703,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
604
703
|
return chatModel.modelName ?? chatModel.name;
|
|
605
704
|
}
|
|
606
705
|
|
|
607
|
-
private extractModelUsageMetrics(response: unknown): Readonly<Record<string, number | undefined>> {
|
|
608
|
-
const usage = this.extractUsageObject(response);
|
|
609
|
-
const inputTokens = this.readUsageNumber(usage, ["input_tokens", "inputTokens", "prompt_tokens", "promptTokens"]);
|
|
610
|
-
const outputTokens = this.readUsageNumber(usage, [
|
|
611
|
-
"output_tokens",
|
|
612
|
-
"outputTokens",
|
|
613
|
-
"completion_tokens",
|
|
614
|
-
"completionTokens",
|
|
615
|
-
]);
|
|
616
|
-
const totalTokens =
|
|
617
|
-
this.readUsageNumber(usage, ["total_tokens", "totalTokens"]) ??
|
|
618
|
-
(inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
|
|
619
|
-
const cachedInputTokens = this.readUsageNumber(usage, [
|
|
620
|
-
"cache_read_input_tokens",
|
|
621
|
-
"cacheReadInputTokens",
|
|
622
|
-
"input_token_details.cached_tokens",
|
|
623
|
-
]);
|
|
624
|
-
const reasoningTokens = this.readUsageNumber(usage, [
|
|
625
|
-
"reasoning_tokens",
|
|
626
|
-
"reasoningTokens",
|
|
627
|
-
"output_token_details.reasoning_tokens",
|
|
628
|
-
]);
|
|
629
|
-
return {
|
|
630
|
-
[GenAiTelemetryAttributeNames.usageInputTokens]: inputTokens,
|
|
631
|
-
[GenAiTelemetryAttributeNames.usageOutputTokens]: outputTokens,
|
|
632
|
-
[GenAiTelemetryAttributeNames.usageTotalTokens]: totalTokens,
|
|
633
|
-
[GenAiTelemetryAttributeNames.usageCacheReadInputTokens]: cachedInputTokens,
|
|
634
|
-
[GenAiTelemetryAttributeNames.usageReasoningTokens]: reasoningTokens,
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
private extractUsageObject(response: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
639
|
-
if (!this.isRecord(response)) {
|
|
640
|
-
return undefined;
|
|
641
|
-
}
|
|
642
|
-
const usageMetadata = response["usage_metadata"];
|
|
643
|
-
if (this.isRecord(usageMetadata)) {
|
|
644
|
-
return usageMetadata;
|
|
645
|
-
}
|
|
646
|
-
const responseMetadata = response["response_metadata"];
|
|
647
|
-
if (this.isRecord(responseMetadata)) {
|
|
648
|
-
const tokenUsage = responseMetadata["tokenUsage"];
|
|
649
|
-
if (this.isRecord(tokenUsage)) {
|
|
650
|
-
return tokenUsage;
|
|
651
|
-
}
|
|
652
|
-
const usage = responseMetadata["usage"];
|
|
653
|
-
if (this.isRecord(usage)) {
|
|
654
|
-
return usage;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return undefined;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
private readUsageNumber(
|
|
661
|
-
source: Readonly<Record<string, unknown>> | undefined,
|
|
662
|
-
keys: ReadonlyArray<string>,
|
|
663
|
-
): number | undefined {
|
|
664
|
-
for (const key of keys) {
|
|
665
|
-
const value = this.readNestedUsageValue(source, key);
|
|
666
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
667
|
-
return value;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
return undefined;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
private readNestedUsageValue(source: Readonly<Record<string, unknown>> | undefined, dottedKey: string): unknown {
|
|
674
|
-
if (!source) {
|
|
675
|
-
return undefined;
|
|
676
|
-
}
|
|
677
|
-
let current: unknown = source;
|
|
678
|
-
for (const segment of dottedKey.split(".")) {
|
|
679
|
-
if (!this.isRecord(current)) {
|
|
680
|
-
return undefined;
|
|
681
|
-
}
|
|
682
|
-
current = current[segment];
|
|
683
|
-
}
|
|
684
|
-
return current;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
private isRecord(value: unknown): value is Record<string, unknown> {
|
|
688
|
-
return typeof value === "object" && value !== null;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
706
|
private async markQueuedTools(
|
|
692
707
|
plannedToolCalls: ReadonlyArray<PlannedToolCall>,
|
|
693
708
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
@@ -760,7 +775,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
760
775
|
return effectiveError;
|
|
761
776
|
}
|
|
762
777
|
|
|
763
|
-
private summarizeLlmMessages(messages: ReadonlyArray<
|
|
778
|
+
private summarizeLlmMessages(messages: ReadonlyArray<ModelMessage>): JsonValue {
|
|
764
779
|
const last = messages[messages.length - 1];
|
|
765
780
|
const preview =
|
|
766
781
|
typeof last?.content === "string"
|
|
@@ -775,9 +790,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
775
790
|
}
|
|
776
791
|
|
|
777
792
|
private resultToJsonValue(value: unknown): JsonValue | undefined {
|
|
778
|
-
if (value === undefined)
|
|
779
|
-
return undefined;
|
|
780
|
-
}
|
|
793
|
+
if (value === undefined) return undefined;
|
|
781
794
|
const json = JSON.stringify(value);
|
|
782
795
|
return JSON.parse(json) as JsonValue;
|
|
783
796
|
}
|
|
@@ -787,7 +800,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
787
800
|
itemIndex: number,
|
|
788
801
|
items: Items,
|
|
789
802
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
790
|
-
): ReadonlyArray<
|
|
803
|
+
): ReadonlyArray<ModelMessage> {
|
|
791
804
|
return AgentMessageFactory.createPromptMessages(
|
|
792
805
|
AgentMessageConfigNormalizer.resolveFromInputOrConfig(item.json, ctx.config, {
|
|
793
806
|
item,
|
|
@@ -803,7 +816,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
803
816
|
const inputSchema = config.getInputSchema();
|
|
804
817
|
if (inputSchema == null) {
|
|
805
818
|
throw new Error(
|
|
806
|
-
`AIAgent tool "${config.name}": node-backed tool is missing inputSchema (cannot build
|
|
819
|
+
`AIAgent tool "${config.name}": node-backed tool is missing inputSchema (cannot build AI SDK tool).`,
|
|
807
820
|
);
|
|
808
821
|
}
|
|
809
822
|
return {
|
|
@@ -816,7 +829,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
816
829
|
const inputSchema = config.getInputSchema();
|
|
817
830
|
if (inputSchema == null) {
|
|
818
831
|
throw new Error(
|
|
819
|
-
`AIAgent tool "${config.name}": callable tool is missing inputSchema (cannot build
|
|
832
|
+
`AIAgent tool "${config.name}": callable tool is missing inputSchema (cannot build AI SDK tool).`,
|
|
820
833
|
);
|
|
821
834
|
}
|
|
822
835
|
return {
|
|
@@ -837,11 +850,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
837
850
|
};
|
|
838
851
|
}
|
|
839
852
|
|
|
840
|
-
/**
|
|
841
|
-
* Consumer apps can resolve two copies of `@codemation/core`, breaking `instanceof NodeBackedToolConfig` and
|
|
842
|
-
* sending node-backed tools down the plugin-tool branch with `inputSchema: undefined` (LangChain then crashes in
|
|
843
|
-
* json-schema validation). {@link NodeBackedToolConfig#toolKind} is stable across copies.
|
|
844
|
-
*/
|
|
845
853
|
private isNodeBackedToolConfig(config: ToolConfig): config is NodeBackedToolConfig<any, any, any> {
|
|
846
854
|
return (
|
|
847
855
|
config instanceof NodeBackedToolConfig ||
|
|
@@ -849,9 +857,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
849
857
|
);
|
|
850
858
|
}
|
|
851
859
|
|
|
852
|
-
/**
|
|
853
|
-
* Callable tools use {@link CallableToolConfig#toolKind} for cross-package / JSON round-trip safety.
|
|
854
|
-
*/
|
|
855
860
|
private isCallableToolConfig(config: ToolConfig): config is CallableToolConfig<ZodSchemaAny, ZodSchemaAny> {
|
|
856
861
|
return (
|
|
857
862
|
config instanceof CallableToolConfig ||
|