@codemation/core-nodes 0.4.1 → 0.4.2
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 +16 -0
- package/dist/index.cjs +665 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +194 -5
- package/dist/index.d.ts +194 -5
- package/dist/index.js +648 -87
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/chatModels/openAiChatModelConfig.ts +3 -0
- package/src/nodes/AIAgentNode.ts +294 -102
- package/src/nodes/AgentToolErrorClassifier.ts +106 -0
- package/src/nodes/AgentToolExecutionCoordinator.ts +364 -0
- package/src/nodes/AgentToolRepair.types.ts +26 -0
- package/src/nodes/AgentToolRepairExhaustedError.ts +51 -0
- package/src/nodes/AgentToolRepairPolicy.ts +21 -0
- package/src/nodes/aiAgent.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemation/core-nodes",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@langchain/core": "^1.1.31",
|
|
32
32
|
"@langchain/openai": "^1.2.12",
|
|
33
33
|
"lucide-react": "^0.577.0",
|
|
34
|
-
"@codemation/core": "0.
|
|
34
|
+
"@codemation/core": "0.8.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.3.5",
|
|
@@ -6,6 +6,8 @@ import { OpenAIChatModelFactory } from "./OpenAIChatModelFactory";
|
|
|
6
6
|
export class OpenAIChatModelConfig implements ChatModelConfig {
|
|
7
7
|
readonly type = OpenAIChatModelFactory;
|
|
8
8
|
readonly presentation: AgentCanvasPresentation<CanvasIconName>;
|
|
9
|
+
readonly provider = "openai";
|
|
10
|
+
readonly modelName: string;
|
|
9
11
|
|
|
10
12
|
constructor(
|
|
11
13
|
public readonly name: string,
|
|
@@ -17,6 +19,7 @@ export class OpenAIChatModelConfig implements ChatModelConfig {
|
|
|
17
19
|
maxTokens?: number;
|
|
18
20
|
}>,
|
|
19
21
|
) {
|
|
22
|
+
this.modelName = model;
|
|
20
23
|
this.presentation = presentationIn ?? { icon: "builtin:openai", label: name };
|
|
21
24
|
}
|
|
22
25
|
|
package/src/nodes/AIAgentNode.ts
CHANGED
|
@@ -22,9 +22,12 @@ import {
|
|
|
22
22
|
AgentGuardrailDefaults,
|
|
23
23
|
AgentMessageConfigNormalizer,
|
|
24
24
|
CallableToolConfig,
|
|
25
|
+
CodemationTelemetryAttributeNames,
|
|
26
|
+
CodemationTelemetryMetricNames,
|
|
25
27
|
ConnectionInvocationIdFactory,
|
|
26
28
|
ConnectionNodeIdFactory,
|
|
27
29
|
CoreTokens,
|
|
30
|
+
GenAiTelemetryAttributeNames,
|
|
28
31
|
NodeBackedToolConfig,
|
|
29
32
|
inject,
|
|
30
33
|
node,
|
|
@@ -36,6 +39,7 @@ import { z } from "zod";
|
|
|
36
39
|
|
|
37
40
|
import type { AIAgent } from "./AIAgentConfig";
|
|
38
41
|
import { AIAgentExecutionHelpersFactory } from "./AIAgentExecutionHelpersFactory";
|
|
42
|
+
import { AgentToolExecutionCoordinator } from "./AgentToolExecutionCoordinator";
|
|
39
43
|
import { ConnectionCredentialExecutionContextFactory } from "./ConnectionCredentialExecutionContextFactory";
|
|
40
44
|
import { AgentMessageFactory } from "./AgentMessageFactory";
|
|
41
45
|
import { AgentOutputFactory } from "./AgentOutputFactory";
|
|
@@ -91,6 +95,8 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
91
95
|
private readonly executionHelpers: AIAgentExecutionHelpersFactory,
|
|
92
96
|
@inject(AgentStructuredOutputRunner)
|
|
93
97
|
private readonly structuredOutputRunner: AgentStructuredOutputRunner,
|
|
98
|
+
@inject(AgentToolExecutionCoordinator)
|
|
99
|
+
private readonly toolExecutionCoordinator: AgentToolExecutionCoordinator,
|
|
94
100
|
) {
|
|
95
101
|
this.connectionCredentialExecutionContextFactory =
|
|
96
102
|
this.executionHelpers.createConnectionCredentialExecutionContextFactory(credentialSessions);
|
|
@@ -178,21 +184,28 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
178
184
|
prepared.guardrails.modelInvocationOptions,
|
|
179
185
|
),
|
|
180
186
|
});
|
|
187
|
+
await ctx.telemetry.recordMetric({ name: CodemationTelemetryMetricNames.agentTurns, value: 1 });
|
|
188
|
+
await ctx.telemetry.recordMetric({ name: CodemationTelemetryMetricNames.agentToolCalls, value: 0 });
|
|
181
189
|
return this.buildOutputItem(item, structuredOutput);
|
|
182
190
|
}
|
|
183
191
|
const modelWithTools = this.bindToolsToModel(prepared.model, itemScopedTools);
|
|
184
|
-
const
|
|
192
|
+
const loopResult = await this.runTurnLoopUntilFinalAnswer({
|
|
185
193
|
prepared,
|
|
186
194
|
itemInputsByPort,
|
|
187
195
|
itemScopedTools,
|
|
188
196
|
conversation,
|
|
189
197
|
modelWithTools,
|
|
190
198
|
});
|
|
199
|
+
await ctx.telemetry.recordMetric({ name: CodemationTelemetryMetricNames.agentTurns, value: loopResult.turnCount });
|
|
200
|
+
await ctx.telemetry.recordMetric({
|
|
201
|
+
name: CodemationTelemetryMetricNames.agentToolCalls,
|
|
202
|
+
value: loopResult.toolCallCount,
|
|
203
|
+
});
|
|
191
204
|
const outputJson = await this.resolveFinalOutputJson(
|
|
192
205
|
prepared,
|
|
193
206
|
itemInputsByPort,
|
|
194
207
|
conversation,
|
|
195
|
-
finalResponse,
|
|
208
|
+
loopResult.finalResponse,
|
|
196
209
|
itemScopedTools.length > 0,
|
|
197
210
|
);
|
|
198
211
|
return this.buildOutputItem(item, outputJson);
|
|
@@ -207,13 +220,17 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
207
220
|
itemScopedTools: ReadonlyArray<ItemScopedToolBinding>;
|
|
208
221
|
conversation: BaseMessage[];
|
|
209
222
|
modelWithTools: LangChainChatModelLike;
|
|
210
|
-
}): Promise<AIMessage
|
|
223
|
+
}): Promise<Readonly<{ finalResponse: AIMessage; turnCount: number; toolCallCount: number }>> {
|
|
211
224
|
const { prepared, itemInputsByPort, itemScopedTools, conversation, modelWithTools } = args;
|
|
212
225
|
const { ctx, guardrails, languageModelConnectionNodeId } = prepared;
|
|
213
226
|
|
|
214
227
|
let finalResponse: AIMessage | undefined;
|
|
228
|
+
let toolCallCount = 0;
|
|
229
|
+
let turnCount = 0;
|
|
230
|
+
const repairAttemptsByToolName = new Map<string, number>();
|
|
215
231
|
|
|
216
232
|
for (let turn = 1; turn <= guardrails.maxTurns; turn++) {
|
|
233
|
+
turnCount = turn;
|
|
217
234
|
const response = await this.invokeModel(
|
|
218
235
|
modelWithTools,
|
|
219
236
|
languageModelConnectionNodeId,
|
|
@@ -235,15 +252,25 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
235
252
|
}
|
|
236
253
|
|
|
237
254
|
const plannedToolCalls = this.planToolCalls(itemScopedTools, toolCalls, ctx.nodeId);
|
|
255
|
+
toolCallCount += plannedToolCalls.length;
|
|
238
256
|
await this.markQueuedTools(plannedToolCalls, ctx);
|
|
239
|
-
const executedToolCalls = await this.
|
|
257
|
+
const executedToolCalls = await this.toolExecutionCoordinator.execute({
|
|
258
|
+
plannedToolCalls,
|
|
259
|
+
ctx,
|
|
260
|
+
agentName: this.getAgentDisplayName(ctx),
|
|
261
|
+
repairAttemptsByToolName,
|
|
262
|
+
});
|
|
240
263
|
this.appendAssistantAndToolMessages(conversation, response, executedToolCalls);
|
|
241
264
|
}
|
|
242
265
|
|
|
243
266
|
if (!finalResponse) {
|
|
244
267
|
throw new Error(`AIAgent "${ctx.config.name ?? ctx.nodeId}" did not produce a model response.`);
|
|
245
268
|
}
|
|
246
|
-
return
|
|
269
|
+
return {
|
|
270
|
+
finalResponse,
|
|
271
|
+
turnCount,
|
|
272
|
+
toolCallCount,
|
|
273
|
+
};
|
|
247
274
|
}
|
|
248
275
|
|
|
249
276
|
private cannotExecuteAnotherToolRound(turn: number, guardrails: ResolvedGuardrails): boolean {
|
|
@@ -374,10 +401,15 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
374
401
|
inputsByPort: NodeInputsByPort,
|
|
375
402
|
options?: AgentGuardrailConfig["modelInvocationOptions"],
|
|
376
403
|
): Promise<AIMessage> {
|
|
404
|
+
const invocationId = ConnectionInvocationIdFactory.create();
|
|
405
|
+
const startedAt = new Date();
|
|
406
|
+
const summarizedInput = this.summarizeLlmMessages(messages);
|
|
407
|
+
const span = this.createModelInvocationSpan(ctx, invocationId, startedAt);
|
|
377
408
|
await ctx.nodeState?.markQueued({ nodeId, activationId: ctx.activationId, inputsByPort });
|
|
378
409
|
await ctx.nodeState?.markRunning({ nodeId, activationId: ctx.activationId, inputsByPort });
|
|
379
410
|
try {
|
|
380
411
|
const response = (await model.invoke(messages, options)) as AIMessage;
|
|
412
|
+
const finishedAt = new Date();
|
|
381
413
|
await ctx.nodeState?.markCompleted({
|
|
382
414
|
nodeId,
|
|
383
415
|
activationId: ctx.activationId,
|
|
@@ -387,19 +419,46 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
387
419
|
}),
|
|
388
420
|
});
|
|
389
421
|
const content = AgentMessageFactory.extractContent(response);
|
|
422
|
+
await span.attachArtifact({
|
|
423
|
+
kind: "ai.messages",
|
|
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);
|
|
433
|
+
await span.end({ status: "ok", endedAt: finishedAt });
|
|
390
434
|
await ctx.nodeState?.appendConnectionInvocation({
|
|
391
|
-
invocationId
|
|
435
|
+
invocationId,
|
|
392
436
|
connectionNodeId: nodeId,
|
|
393
437
|
parentAgentNodeId: ctx.nodeId,
|
|
394
438
|
parentAgentActivationId: ctx.activationId,
|
|
395
439
|
status: "completed",
|
|
396
|
-
managedInput:
|
|
440
|
+
managedInput: summarizedInput,
|
|
397
441
|
managedOutput: content,
|
|
398
|
-
|
|
442
|
+
queuedAt: startedAt.toISOString(),
|
|
443
|
+
startedAt: startedAt.toISOString(),
|
|
444
|
+
finishedAt: finishedAt.toISOString(),
|
|
399
445
|
});
|
|
400
446
|
return response;
|
|
401
447
|
} catch (error) {
|
|
402
|
-
|
|
448
|
+
await span.end({
|
|
449
|
+
status: "error",
|
|
450
|
+
statusMessage: error instanceof Error ? error.message : String(error),
|
|
451
|
+
endedAt: new Date(),
|
|
452
|
+
});
|
|
453
|
+
throw await this.failTrackedNodeInvocation({
|
|
454
|
+
error,
|
|
455
|
+
invocationId,
|
|
456
|
+
startedAt,
|
|
457
|
+
nodeId,
|
|
458
|
+
ctx,
|
|
459
|
+
inputsByPort,
|
|
460
|
+
managedInput: this.summarizeLlmMessages(messages),
|
|
461
|
+
});
|
|
403
462
|
}
|
|
404
463
|
}
|
|
405
464
|
|
|
@@ -411,30 +470,222 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
411
470
|
inputsByPort: NodeInputsByPort,
|
|
412
471
|
options?: AgentGuardrailConfig["modelInvocationOptions"],
|
|
413
472
|
): Promise<unknown> {
|
|
473
|
+
const invocationId = ConnectionInvocationIdFactory.create();
|
|
474
|
+
const startedAt = new Date();
|
|
475
|
+
const summarizedInput = this.summarizeLlmMessages(messages);
|
|
476
|
+
const span = this.createModelInvocationSpan(ctx, invocationId, startedAt);
|
|
414
477
|
await ctx.nodeState?.markQueued({ nodeId, activationId: ctx.activationId, inputsByPort });
|
|
415
478
|
await ctx.nodeState?.markRunning({ nodeId, activationId: ctx.activationId, inputsByPort });
|
|
416
479
|
try {
|
|
417
480
|
const response = await model.invoke(messages, options);
|
|
481
|
+
const finishedAt = new Date();
|
|
418
482
|
await ctx.nodeState?.markCompleted({
|
|
419
483
|
nodeId,
|
|
420
484
|
activationId: ctx.activationId,
|
|
421
485
|
inputsByPort,
|
|
422
486
|
outputs: AgentOutputFactory.fromUnknown(response),
|
|
423
487
|
});
|
|
488
|
+
await span.attachArtifact({
|
|
489
|
+
kind: "ai.messages",
|
|
490
|
+
contentType: "application/json",
|
|
491
|
+
previewJson: summarizedInput,
|
|
492
|
+
});
|
|
493
|
+
await span.attachArtifact({
|
|
494
|
+
kind: "ai.response.structured",
|
|
495
|
+
contentType: "application/json",
|
|
496
|
+
previewJson: this.resultToJsonValue(response),
|
|
497
|
+
});
|
|
498
|
+
await this.recordModelUsageMetrics(span, response, ctx);
|
|
499
|
+
await span.end({ status: "ok", endedAt: finishedAt });
|
|
424
500
|
await ctx.nodeState?.appendConnectionInvocation({
|
|
425
|
-
invocationId
|
|
501
|
+
invocationId,
|
|
426
502
|
connectionNodeId: nodeId,
|
|
427
503
|
parentAgentNodeId: ctx.nodeId,
|
|
428
504
|
parentAgentActivationId: ctx.activationId,
|
|
429
505
|
status: "completed",
|
|
430
|
-
managedInput:
|
|
506
|
+
managedInput: summarizedInput,
|
|
431
507
|
managedOutput: this.resultToJsonValue(response),
|
|
432
|
-
|
|
508
|
+
queuedAt: startedAt.toISOString(),
|
|
509
|
+
startedAt: startedAt.toISOString(),
|
|
510
|
+
finishedAt: finishedAt.toISOString(),
|
|
433
511
|
});
|
|
434
512
|
return response;
|
|
435
513
|
} catch (error) {
|
|
436
|
-
|
|
514
|
+
await span.end({
|
|
515
|
+
status: "error",
|
|
516
|
+
statusMessage: error instanceof Error ? error.message : String(error),
|
|
517
|
+
endedAt: new Date(),
|
|
518
|
+
});
|
|
519
|
+
throw await this.failTrackedNodeInvocation({
|
|
520
|
+
error,
|
|
521
|
+
invocationId,
|
|
522
|
+
startedAt,
|
|
523
|
+
nodeId,
|
|
524
|
+
ctx,
|
|
525
|
+
inputsByPort,
|
|
526
|
+
managedInput: this.summarizeLlmMessages(messages),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private createModelInvocationSpan(
|
|
532
|
+
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
533
|
+
invocationId: string,
|
|
534
|
+
startedAt: Date,
|
|
535
|
+
) {
|
|
536
|
+
return ctx.telemetry.startChildSpan({
|
|
537
|
+
name: "gen_ai.chat.completion",
|
|
538
|
+
kind: "client",
|
|
539
|
+
startedAt,
|
|
540
|
+
attributes: {
|
|
541
|
+
[CodemationTelemetryAttributeNames.connectionInvocationId]: invocationId,
|
|
542
|
+
[GenAiTelemetryAttributeNames.operationName]: "chat",
|
|
543
|
+
[GenAiTelemetryAttributeNames.requestModel]: this.resolveChatModelName(ctx.config.chatModel),
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private async recordModelUsageMetrics(
|
|
549
|
+
span: ReturnType<NodeExecutionContext["telemetry"]["startChildSpan"]>,
|
|
550
|
+
response: unknown,
|
|
551
|
+
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
552
|
+
) {
|
|
553
|
+
const usage = this.extractModelUsageMetrics(response);
|
|
554
|
+
for (const [name, value] of Object.entries(usage)) {
|
|
555
|
+
if (value === undefined) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
await span.recordMetric({ name, value });
|
|
437
559
|
}
|
|
560
|
+
await this.captureCostTrackingUsage(span, ctx, usage);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private async captureCostTrackingUsage(
|
|
564
|
+
span: ReturnType<NodeExecutionContext["telemetry"]["startChildSpan"]>,
|
|
565
|
+
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
566
|
+
usage: Readonly<Record<string, number | undefined>>,
|
|
567
|
+
): Promise<void> {
|
|
568
|
+
const costTracking = span.costTracking;
|
|
569
|
+
if (!costTracking) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const provider = ctx.config.chatModel.provider;
|
|
573
|
+
const pricingKey = ctx.config.chatModel.modelName;
|
|
574
|
+
if (!provider || !pricingKey) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const inputTokens = usage[GenAiTelemetryAttributeNames.usageInputTokens];
|
|
578
|
+
const outputTokens = usage[GenAiTelemetryAttributeNames.usageOutputTokens];
|
|
579
|
+
if (inputTokens !== undefined) {
|
|
580
|
+
await costTracking.captureUsage({
|
|
581
|
+
component: "chat",
|
|
582
|
+
provider,
|
|
583
|
+
operation: "completion.input",
|
|
584
|
+
pricingKey,
|
|
585
|
+
usageUnit: "input_tokens",
|
|
586
|
+
quantity: inputTokens,
|
|
587
|
+
modelName: pricingKey,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (outputTokens !== undefined) {
|
|
591
|
+
await costTracking.captureUsage({
|
|
592
|
+
component: "chat",
|
|
593
|
+
provider,
|
|
594
|
+
operation: "completion.output",
|
|
595
|
+
pricingKey,
|
|
596
|
+
usageUnit: "output_tokens",
|
|
597
|
+
quantity: outputTokens,
|
|
598
|
+
modelName: pricingKey,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private resolveChatModelName(chatModel: ChatModelConfig): string {
|
|
604
|
+
return chatModel.modelName ?? chatModel.name;
|
|
605
|
+
}
|
|
606
|
+
|
|
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;
|
|
438
689
|
}
|
|
439
690
|
|
|
440
691
|
private async markQueuedTools(
|
|
@@ -450,65 +701,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
450
701
|
}
|
|
451
702
|
}
|
|
452
703
|
|
|
453
|
-
private async executeToolCalls(
|
|
454
|
-
plannedToolCalls: ReadonlyArray<PlannedToolCall>,
|
|
455
|
-
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
456
|
-
): Promise<ReadonlyArray<ExecutedToolCall>> {
|
|
457
|
-
const results = await Promise.allSettled(
|
|
458
|
-
plannedToolCalls.map(async (plannedToolCall) => {
|
|
459
|
-
const toolCallInputsByPort = AgentToolCallPortMap.fromInput(plannedToolCall.toolCall.input ?? {});
|
|
460
|
-
await ctx.nodeState?.markRunning({
|
|
461
|
-
nodeId: plannedToolCall.nodeId,
|
|
462
|
-
activationId: ctx.activationId,
|
|
463
|
-
inputsByPort: toolCallInputsByPort,
|
|
464
|
-
});
|
|
465
|
-
try {
|
|
466
|
-
const serialized = await plannedToolCall.binding.langChainTool.invoke(plannedToolCall.toolCall.input ?? {});
|
|
467
|
-
const result = this.parseToolOutput(serialized);
|
|
468
|
-
await ctx.nodeState?.markCompleted({
|
|
469
|
-
nodeId: plannedToolCall.nodeId,
|
|
470
|
-
activationId: ctx.activationId,
|
|
471
|
-
inputsByPort: toolCallInputsByPort,
|
|
472
|
-
outputs: AgentOutputFactory.fromUnknown(result),
|
|
473
|
-
});
|
|
474
|
-
await ctx.nodeState?.appendConnectionInvocation({
|
|
475
|
-
invocationId: ConnectionInvocationIdFactory.create(),
|
|
476
|
-
connectionNodeId: plannedToolCall.nodeId,
|
|
477
|
-
parentAgentNodeId: ctx.nodeId,
|
|
478
|
-
parentAgentActivationId: ctx.activationId,
|
|
479
|
-
status: "completed",
|
|
480
|
-
managedInput: this.toolCallInputToJson(plannedToolCall.toolCall.input),
|
|
481
|
-
managedOutput: this.resultToJsonValue(result),
|
|
482
|
-
finishedAt: new Date().toISOString(),
|
|
483
|
-
});
|
|
484
|
-
return {
|
|
485
|
-
toolName: plannedToolCall.binding.config.name,
|
|
486
|
-
toolCallId: plannedToolCall.toolCall.id ?? plannedToolCall.binding.config.name,
|
|
487
|
-
serialized,
|
|
488
|
-
result,
|
|
489
|
-
} satisfies ExecutedToolCall;
|
|
490
|
-
} catch (error) {
|
|
491
|
-
throw await this.failTrackedNodeInvocation(
|
|
492
|
-
error,
|
|
493
|
-
plannedToolCall.nodeId,
|
|
494
|
-
ctx,
|
|
495
|
-
toolCallInputsByPort,
|
|
496
|
-
this.toolCallInputToJson(plannedToolCall.toolCall.input),
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
}),
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
const rejected = results.find((result) => result.status === "rejected");
|
|
503
|
-
if (rejected?.status === "rejected") {
|
|
504
|
-
throw rejected.reason instanceof Error ? rejected.reason : new Error(String(rejected.reason));
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return results
|
|
508
|
-
.filter((result): result is PromiseFulfilledResult<ExecutedToolCall> => result.status === "fulfilled")
|
|
509
|
-
.map((result) => result.value);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
704
|
private planToolCalls(
|
|
513
705
|
bindings: ReadonlyArray<ItemScopedToolBinding>,
|
|
514
706
|
toolCalls: ReadonlyArray<AgentToolCall>,
|
|
@@ -529,42 +721,41 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
529
721
|
});
|
|
530
722
|
}
|
|
531
723
|
|
|
532
|
-
private parseToolOutput(serialized: unknown): unknown {
|
|
533
|
-
if (typeof serialized !== "string") return serialized;
|
|
534
|
-
try {
|
|
535
|
-
return JSON.parse(serialized);
|
|
536
|
-
} catch {
|
|
537
|
-
return serialized;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
724
|
private async failTrackedNodeInvocation(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
725
|
+
args: Readonly<{
|
|
726
|
+
error: unknown;
|
|
727
|
+
invocationId: string;
|
|
728
|
+
startedAt: Date;
|
|
729
|
+
nodeId: string;
|
|
730
|
+
ctx: NodeExecutionContext<AIAgent<any, any>>;
|
|
731
|
+
inputsByPort: NodeInputsByPort;
|
|
732
|
+
managedInput?: JsonValue;
|
|
733
|
+
}>,
|
|
547
734
|
): Promise<Error> {
|
|
548
|
-
const effectiveError = error instanceof Error ? error : new Error(String(error));
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
735
|
+
const effectiveError = args.error instanceof Error ? args.error : new Error(String(args.error));
|
|
736
|
+
const finishedAt = new Date();
|
|
737
|
+
await args.ctx.nodeState?.markFailed({
|
|
738
|
+
nodeId: args.nodeId,
|
|
739
|
+
activationId: args.ctx.activationId,
|
|
740
|
+
inputsByPort: args.inputsByPort,
|
|
553
741
|
error: effectiveError,
|
|
554
742
|
});
|
|
555
|
-
await ctx.nodeState?.appendConnectionInvocation({
|
|
556
|
-
invocationId:
|
|
557
|
-
connectionNodeId: nodeId,
|
|
558
|
-
parentAgentNodeId: ctx.nodeId,
|
|
559
|
-
parentAgentActivationId: ctx.activationId,
|
|
743
|
+
await args.ctx.nodeState?.appendConnectionInvocation({
|
|
744
|
+
invocationId: args.invocationId,
|
|
745
|
+
connectionNodeId: args.nodeId,
|
|
746
|
+
parentAgentNodeId: args.ctx.nodeId,
|
|
747
|
+
parentAgentActivationId: args.ctx.activationId,
|
|
560
748
|
status: "failed",
|
|
561
|
-
managedInput,
|
|
749
|
+
managedInput: args.managedInput,
|
|
562
750
|
error: {
|
|
563
751
|
message: effectiveError.message,
|
|
564
752
|
name: effectiveError.name,
|
|
565
753
|
stack: effectiveError.stack,
|
|
754
|
+
details: this.extractErrorDetails(effectiveError),
|
|
566
755
|
},
|
|
567
|
-
|
|
756
|
+
queuedAt: args.startedAt.toISOString(),
|
|
757
|
+
startedAt: args.startedAt.toISOString(),
|
|
758
|
+
finishedAt: finishedAt.toISOString(),
|
|
568
759
|
});
|
|
569
760
|
return effectiveError;
|
|
570
761
|
}
|
|
@@ -583,10 +774,6 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
583
774
|
};
|
|
584
775
|
}
|
|
585
776
|
|
|
586
|
-
private toolCallInputToJson(input: unknown): JsonValue | undefined {
|
|
587
|
-
return this.resultToJsonValue(input);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
777
|
private resultToJsonValue(value: unknown): JsonValue | undefined {
|
|
591
778
|
if (value === undefined) {
|
|
592
779
|
return undefined;
|
|
@@ -687,4 +874,9 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
687
874
|
private getAgentDisplayName(ctx: NodeExecutionContext<AIAgent<any, any>>): string {
|
|
688
875
|
return ctx.config.name ?? ctx.nodeId;
|
|
689
876
|
}
|
|
877
|
+
|
|
878
|
+
private extractErrorDetails(error: Error): JsonValue | undefined {
|
|
879
|
+
const candidate = error as Error & { details?: JsonValue };
|
|
880
|
+
return candidate.details;
|
|
881
|
+
}
|
|
690
882
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { JsonValue } from "@codemation/core";
|
|
2
|
+
import { injectable } from "@codemation/core";
|
|
3
|
+
import { ZodError } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { AgentToolFailureClassification, AgentToolValidationIssue } from "./AgentToolRepair.types";
|
|
6
|
+
|
|
7
|
+
@injectable()
|
|
8
|
+
export class AgentToolErrorClassifier {
|
|
9
|
+
classify(
|
|
10
|
+
args: Readonly<{
|
|
11
|
+
error: unknown;
|
|
12
|
+
toolName: string;
|
|
13
|
+
schema?: unknown;
|
|
14
|
+
}>,
|
|
15
|
+
): AgentToolFailureClassification {
|
|
16
|
+
const effectiveError = this.toError(args.error);
|
|
17
|
+
if (this.isRepairableValidationError(args.error, effectiveError)) {
|
|
18
|
+
return {
|
|
19
|
+
kind: "repairable_validation_error",
|
|
20
|
+
effectiveError,
|
|
21
|
+
issues: this.extractIssues(args.error, effectiveError, args.toolName),
|
|
22
|
+
requiredSchemaReminder: this.toJsonValue(args.schema),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (this.isTransientExecutionError(effectiveError)) {
|
|
26
|
+
return {
|
|
27
|
+
kind: "transient_execution_error",
|
|
28
|
+
effectiveError,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
kind: "non_repairable_error",
|
|
33
|
+
effectiveError,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private isRepairableValidationError(rawError: unknown, effectiveError: Error): boolean {
|
|
38
|
+
if (rawError instanceof ZodError) {
|
|
39
|
+
const stage = (rawError as ZodError & { codemationToolValidationStage?: "input" | "output" })
|
|
40
|
+
.codemationToolValidationStage;
|
|
41
|
+
return stage !== "output";
|
|
42
|
+
}
|
|
43
|
+
if (effectiveError.name === "ZodError") {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return effectiveError.message.includes("Received tool input did not match expected schema");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private extractIssues(
|
|
50
|
+
rawError: unknown,
|
|
51
|
+
effectiveError: Error,
|
|
52
|
+
toolName: string,
|
|
53
|
+
): ReadonlyArray<AgentToolValidationIssue> | undefined {
|
|
54
|
+
if (rawError instanceof ZodError) {
|
|
55
|
+
return rawError.issues.map((issue) => ({
|
|
56
|
+
path: issue.path.map((segment) => (typeof segment === "number" ? segment : String(segment))),
|
|
57
|
+
code: issue.code,
|
|
58
|
+
message: issue.message,
|
|
59
|
+
expected: this.toOptionalString("expected" in issue ? issue.expected : undefined),
|
|
60
|
+
received: this.toOptionalString("received" in issue ? issue.received : undefined),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
if (effectiveError.name !== "ZodError") {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
path: [],
|
|
69
|
+
code: "invalid_tool_input",
|
|
70
|
+
message: `Tool "${toolName}" input was invalid: ${effectiveError.message}`,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private isTransientExecutionError(error: Error): boolean {
|
|
76
|
+
const summary = `${error.name} ${error.message}`.toLowerCase();
|
|
77
|
+
return (
|
|
78
|
+
summary.includes("timeout") ||
|
|
79
|
+
summary.includes("timed out") ||
|
|
80
|
+
summary.includes("rate limit") ||
|
|
81
|
+
summary.includes("too many requests") ||
|
|
82
|
+
summary.includes("temporarily unavailable") ||
|
|
83
|
+
summary.includes("econnreset") ||
|
|
84
|
+
summary.includes("etimedout") ||
|
|
85
|
+
summary.includes("503")
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private toError(error: unknown): Error {
|
|
90
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private toJsonValue(value: unknown): JsonValue | undefined {
|
|
94
|
+
if (value === undefined) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return JSON.parse(JSON.stringify(value)) as JsonValue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private toOptionalString(value: unknown): string | undefined {
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return String(value);
|
|
105
|
+
}
|
|
106
|
+
}
|