@codemation/core-nodes 0.4.2 → 0.6.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/dist/index.cjs +3485 -474
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1763 -685
  5. package/dist/index.d.ts +1763 -685
  6. package/dist/index.js +3452 -479
  7. package/dist/index.js.map +1 -1
  8. package/package.json +8 -5
  9. package/src/authoring/defineRestNode.types.ts +204 -0
  10. package/src/chatModels/OpenAIChatModelFactory.ts +17 -8
  11. package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +123 -0
  12. package/src/credentials/ApiKeyCredentialType.ts +60 -0
  13. package/src/credentials/BasicAuthCredentialType.ts +51 -0
  14. package/src/credentials/BearerTokenCredentialType.ts +40 -0
  15. package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +117 -0
  16. package/src/credentials/OAuth2TokenExchangeFactory.ts +52 -0
  17. package/src/credentials/index.ts +4 -0
  18. package/src/http/HttpBodyBuilder.ts +90 -0
  19. package/src/http/HttpRequestExecutor.ts +150 -0
  20. package/src/http/HttpUrlBuilder.ts +22 -0
  21. package/src/http/httpRequest.types.ts +69 -0
  22. package/src/index.ts +10 -1
  23. package/src/nodes/AIAgentExecutionHelpersFactory.ts +45 -59
  24. package/src/nodes/AIAgentNode.ts +391 -288
  25. package/src/nodes/AgentMessageFactory.ts +57 -49
  26. package/src/nodes/AgentStructuredOutputRunner.ts +65 -71
  27. package/src/nodes/AgentToolExecutionCoordinator.ts +31 -16
  28. package/src/nodes/AssertionNode.ts +42 -0
  29. package/src/nodes/CronTriggerFactory.ts +45 -0
  30. package/src/nodes/CronTriggerNode.ts +40 -0
  31. package/src/nodes/HttpRequestNodeFactory.ts +99 -23
  32. package/src/nodes/IsTestRunNode.ts +25 -0
  33. package/src/nodes/NodeBackedToolRuntime.ts +40 -4
  34. package/src/nodes/TestTriggerNode.ts +33 -0
  35. package/src/nodes/WebhookTriggerFactory.ts +1 -1
  36. package/src/nodes/aggregate.ts +1 -1
  37. package/src/nodes/aiAgentSupport.types.ts +22 -2
  38. package/src/nodes/assertion.ts +42 -0
  39. package/src/nodes/collections/collectionDeleteNode.types.ts +23 -0
  40. package/src/nodes/collections/collectionFindOneNode.types.ts +26 -0
  41. package/src/nodes/collections/collectionGetNode.types.ts +26 -0
  42. package/src/nodes/collections/collectionInsertNode.types.ts +22 -0
  43. package/src/nodes/collections/collectionListNode.types.ts +30 -0
  44. package/src/nodes/collections/collectionUpdateNode.types.ts +23 -0
  45. package/src/nodes/collections/index.ts +6 -0
  46. package/src/nodes/httpRequest.ts +62 -1
  47. package/src/nodes/if.ts +1 -1
  48. package/src/nodes/isTestRun.ts +24 -0
  49. package/src/nodes/mapData.ts +1 -0
  50. package/src/nodes/merge.ts +1 -1
  51. package/src/nodes/noOp.ts +1 -0
  52. package/src/nodes/split.ts +1 -1
  53. package/src/nodes/testTrigger.ts +72 -0
  54. package/src/nodes/wait.ts +1 -0
  55. package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +0 -46
@@ -1,69 +1,77 @@
1
1
  import type { AgentMessageDto, AgentToolCall } from "@codemation/core";
2
2
 
3
- import { AIMessage, HumanMessage, SystemMessage, ToolMessage, type BaseMessage } from "@langchain/core/messages";
3
+ import type { AssistantModelMessage, ModelMessage, ToolModelMessage } from "ai";
4
4
 
5
+ import type { ExecutedToolCall } from "./aiAgentSupport.types";
6
+
7
+ /**
8
+ * AI-SDK-shaped message construction for the AIAgent stack. Emits plain `ModelMessage[]`
9
+ * ( `{ role: 'system' | 'user' | 'assistant' | 'tool', content: ... }` ) as consumed by
10
+ * `generateText({ messages })` from the `ai` package.
11
+ */
5
12
  export class AgentMessageFactory {
6
- static createPromptMessages(messages: ReadonlyArray<AgentMessageDto>): ReadonlyArray<BaseMessage> {
13
+ static createPromptMessages(messages: ReadonlyArray<AgentMessageDto>): ReadonlyArray<ModelMessage> {
7
14
  return messages.map((message) => this.createPromptMessage(message));
8
15
  }
9
16
 
10
- static createSystemPrompt(systemMessage: string): SystemMessage {
11
- return new SystemMessage(systemMessage);
12
- }
13
-
14
- static createUserPrompt(prompt: string): HumanMessage {
15
- return new HumanMessage(prompt);
16
- }
17
-
18
- static createAssistantPrompt(prompt: string): AIMessage {
19
- return new AIMessage(prompt);
20
- }
21
-
22
- static createToolMessage(toolCallId: string, content: string): ToolMessage {
23
- return new ToolMessage({ tool_call_id: toolCallId, content });
24
- }
25
-
26
- static extractContent(message: unknown): string {
27
- if (typeof message === "string") return message;
28
- if (!this.isRecord(message)) return String(message);
29
- const content = message.content;
30
- if (typeof content === "string") return content;
31
- if (Array.isArray(content)) {
32
- return content
33
- .map((part) => {
34
- if (typeof part === "string") return part;
35
- if (this.isRecord(part) && typeof part.text === "string") return part.text;
36
- return JSON.stringify(part);
37
- })
38
- .join("\n");
17
+ /**
18
+ * Builds the assistant message that contains optional text plus one or more tool-call parts,
19
+ * matching the shape AI SDK emits between steps.
20
+ */
21
+ static createAssistantWithToolCalls(
22
+ text: string | undefined,
23
+ toolCalls: ReadonlyArray<AgentToolCall>,
24
+ ): AssistantModelMessage {
25
+ const content: AssistantModelMessage["content"] = [];
26
+ if (text && text.length > 0) {
27
+ content.push({ type: "text", text });
39
28
  }
40
- return JSON.stringify(content);
29
+ for (const toolCall of toolCalls) {
30
+ content.push({
31
+ type: "tool-call",
32
+ toolCallId: toolCall.id ?? toolCall.name,
33
+ toolName: toolCall.name,
34
+ input: toolCall.input ?? {},
35
+ });
36
+ }
37
+ return { role: "assistant", content };
41
38
  }
42
39
 
43
- static extractToolCalls(message: unknown): ReadonlyArray<AgentToolCall> {
44
- if (!this.isRecord(message)) return [];
45
- const toolCalls = message.tool_calls;
46
- if (!Array.isArray(toolCalls)) return [];
47
- return toolCalls
48
- .filter((toolCall) => this.isRecord(toolCall) && typeof toolCall.name === "string")
49
- .map((toolCall) => ({
50
- id: typeof toolCall.id === "string" ? toolCall.id : undefined,
51
- name: toolCall.name as string,
52
- input: this.isRecord(toolCall) && "args" in toolCall ? toolCall.args : undefined,
53
- }));
40
+ /**
41
+ * Builds the `{ role: "tool", content: [{ type: "tool-result", ... }, ...] }` message returned
42
+ * to the model after each tool round.
43
+ */
44
+ static createToolResultsMessage(executedToolCalls: ReadonlyArray<ExecutedToolCall>): ToolModelMessage {
45
+ return {
46
+ role: "tool",
47
+ content: executedToolCalls.map((executed) => ({
48
+ type: "tool-result",
49
+ toolCallId: executed.toolCallId,
50
+ toolName: executed.toolName,
51
+ output: {
52
+ type: "json",
53
+ value: AgentMessageFactory.toToolResultJson(executed.result),
54
+ },
55
+ })),
56
+ };
54
57
  }
55
58
 
56
- private static isRecord(value: unknown): value is Record<string, unknown> {
57
- return typeof value === "object" && value !== null;
59
+ private static toToolResultJson(value: unknown): import("ai").JSONValue {
60
+ if (value === undefined) return null;
61
+ try {
62
+ return JSON.parse(JSON.stringify(value)) as import("ai").JSONValue;
63
+ } catch {
64
+ return String(value);
65
+ }
58
66
  }
59
67
 
60
- private static createPromptMessage(message: AgentMessageDto): BaseMessage {
68
+ private static createPromptMessage(message: AgentMessageDto): ModelMessage {
61
69
  if (message.role === "system") {
62
- return this.createSystemPrompt(message.content);
70
+ return { role: "system", content: message.content };
63
71
  }
64
72
  if (message.role === "assistant") {
65
- return this.createAssistantPrompt(message.content);
73
+ return { role: "assistant", content: message.content };
66
74
  }
67
- return this.createUserPrompt(message.content);
75
+ return { role: "user", content: message.content };
68
76
  }
69
77
  }
@@ -1,18 +1,13 @@
1
- import type {
2
- ChatModelConfig,
3
- ChatModelStructuredOutputOptions,
4
- LangChainChatModelLike,
5
- LangChainStructuredOutputModelLike,
6
- ZodSchemaAny,
7
- } from "@codemation/core";
1
+ import type { ChatLanguageModel, ChatModelConfig, StructuredOutputOptions, ZodSchemaAny } from "@codemation/core";
8
2
  import { inject, injectable } from "@codemation/core";
9
3
 
10
- import { AIMessage, type BaseMessage } from "@langchain/core/messages";
4
+ import type { ModelMessage } from "ai";
11
5
  import { ZodError } from "zod";
12
6
 
13
- import { OpenAIStructuredOutputMethodFactory } from "../chatModels/OpenAIStructuredOutputMethodFactory";
14
- import { AgentMessageFactory } from "./AgentMessageFactory";
7
+ import { OpenAIChatModelFactory } from "../chatModels/OpenAIChatModelFactory";
8
+ import { OpenAiStrictJsonSchemaFactory } from "../chatModels/OpenAiStrictJsonSchemaFactory";
15
9
  import { AgentStructuredOutputRepairPromptFactory } from "./AgentStructuredOutputRepairPromptFactory";
10
+ import { AgentMessageFactory } from "./AgentMessageFactory";
16
11
 
17
12
  interface ParsedStructuredOutputSuccess<TValue> {
18
13
  readonly ok: true;
@@ -27,50 +22,54 @@ interface ParsedStructuredOutputFailure {
27
22
 
28
23
  type ParsedStructuredOutputResult<TValue> = ParsedStructuredOutputSuccess<TValue> | ParsedStructuredOutputFailure;
29
24
 
25
+ export type StructuredOutputSchemaForModel = ZodSchemaAny | Readonly<Record<string, unknown>>;
26
+
27
+ /**
28
+ * Orchestrates a 2-attempt repair loop on top of `generateText({ output: Output.object(...) })`.
29
+ *
30
+ * Strategy:
31
+ * 1. If the caller already has a raw final text (from a prior tool-calling turn), try parsing it
32
+ * directly against the schema — fast path for models that already emit strict JSON.
33
+ * 2. Otherwise, run a native structured-output call via {@link invokeStructuredModel}. For the
34
+ * OpenAI-strict path, a {@link OpenAiStrictJsonSchemaFactory}-built JSON Schema record is
35
+ * handed to AI SDK's `jsonSchema(...)` wrapper (preserves `additionalProperties: false` at
36
+ * every object depth).
37
+ * 3. If the structured call fails (AI_NoObjectGeneratedError / ZodError / schema reject), run a
38
+ * text-mode repair prompt with the validation error appended, up to 2 attempts.
39
+ */
30
40
  @injectable()
31
41
  export class AgentStructuredOutputRunner {
32
42
  private static readonly repairAttemptCount = 2;
43
+ private static readonly structuredOutputSchemaName = "agent_output";
33
44
 
34
45
  constructor(
35
46
  @inject(AgentStructuredOutputRepairPromptFactory)
36
47
  private readonly repairPromptFactory: AgentStructuredOutputRepairPromptFactory,
37
- @inject(OpenAIStructuredOutputMethodFactory)
38
- private readonly openAiStructuredOutputMethodFactory: OpenAIStructuredOutputMethodFactory,
48
+ @inject(OpenAiStrictJsonSchemaFactory)
49
+ private readonly openAiStrictJsonSchemaFactory: OpenAiStrictJsonSchemaFactory,
39
50
  ) {}
40
51
 
41
52
  async resolve<TOutput>(
42
53
  args: Readonly<{
43
- model: LangChainChatModelLike;
54
+ model: ChatLanguageModel;
44
55
  chatModelConfig: ChatModelConfig;
45
56
  schema: ZodSchemaAny;
46
- conversation: ReadonlyArray<BaseMessage>;
47
- rawFinalResponse?: AIMessage;
57
+ conversation: ReadonlyArray<ModelMessage>;
58
+ rawFinalText?: string;
48
59
  agentName: string;
49
60
  nodeId: string;
50
- invokeTextModel: (messages: ReadonlyArray<BaseMessage>) => Promise<AIMessage>;
61
+ invokeTextModel: (messages: ReadonlyArray<ModelMessage>) => Promise<{ text: string }>;
51
62
  invokeStructuredModel: (
52
- model: LangChainStructuredOutputModelLike,
53
- messages: ReadonlyArray<BaseMessage>,
63
+ schema: StructuredOutputSchemaForModel,
64
+ messages: ReadonlyArray<ModelMessage>,
65
+ options: StructuredOutputOptions | undefined,
54
66
  ) => Promise<unknown>;
55
67
  }>,
56
68
  ): Promise<TOutput> {
57
69
  let lastFailure: ParsedStructuredOutputFailure | undefined;
58
70
 
59
- if (args.rawFinalResponse) {
60
- const directResult = this.tryParseAndValidate<TOutput>(
61
- AgentMessageFactory.extractContent(args.rawFinalResponse),
62
- args.schema,
63
- );
64
- if (directResult.ok) {
65
- return directResult.value;
66
- }
67
- lastFailure = directResult;
68
- } else if (!this.supportsNativeStructuredOutput(args.model)) {
69
- const rawResponse = await args.invokeTextModel(args.conversation);
70
- const directResult = this.tryParseAndValidate<TOutput>(
71
- AgentMessageFactory.extractContent(rawResponse),
72
- args.schema,
73
- );
71
+ if (args.rawFinalText !== undefined) {
72
+ const directResult = this.tryParseAndValidate<TOutput>(args.rawFinalText, args.schema);
74
73
  if (directResult.ok) {
75
74
  return directResult.value;
76
75
  }
@@ -78,19 +77,18 @@ export class AgentStructuredOutputRunner {
78
77
  }
79
78
 
80
79
  try {
81
- const nativeStructuredModel = this.createStructuredOutputModel(args.model, args.chatModelConfig, args.schema);
82
- if (nativeStructuredModel) {
83
- const nativeResult = this.tryValidateStructuredValue<TOutput>(
84
- await args.invokeStructuredModel(nativeStructuredModel, args.conversation),
85
- args.schema,
86
- );
87
- if (nativeResult.ok) {
88
- return nativeResult.value;
89
- }
90
- lastFailure = nativeResult;
80
+ const structuredOptions = this.resolveStructuredOutputOptions(args.chatModelConfig);
81
+ const schemaForModel = this.resolveOutputSchemaForModel(args.schema, structuredOptions);
82
+ const nativeResult = this.tryValidateStructuredValue<TOutput>(
83
+ await args.invokeStructuredModel(schemaForModel, args.conversation, structuredOptions),
84
+ args.schema,
85
+ );
86
+ if (nativeResult.ok) {
87
+ return nativeResult.value;
91
88
  }
89
+ lastFailure = nativeResult;
92
90
  } catch (error) {
93
- lastFailure = {
91
+ lastFailure = lastFailure ?? {
94
92
  ok: false,
95
93
  invalidContent: "",
96
94
  validationError: `Native structured output failed: ${this.summarizeError(error)}`,
@@ -112,16 +110,16 @@ export class AgentStructuredOutputRunner {
112
110
  private async retryWithRepairPrompt<TOutput>(
113
111
  args: Readonly<{
114
112
  schema: ZodSchemaAny;
115
- conversation: ReadonlyArray<BaseMessage>;
113
+ conversation: ReadonlyArray<ModelMessage>;
116
114
  lastFailure: ParsedStructuredOutputFailure;
117
115
  agentName: string;
118
116
  nodeId: string;
119
- invokeTextModel: (messages: ReadonlyArray<BaseMessage>) => Promise<AIMessage>;
117
+ invokeTextModel: (messages: ReadonlyArray<ModelMessage>) => Promise<{ text: string }>;
120
118
  }>,
121
119
  ): Promise<TOutput> {
122
120
  let failure = args.lastFailure;
123
121
  for (let attempt = 1; attempt <= AgentStructuredOutputRunner.repairAttemptCount; attempt++) {
124
- const repairMessages = [
122
+ const repairMessages: ReadonlyArray<ModelMessage> = [
125
123
  ...args.conversation,
126
124
  ...AgentMessageFactory.createPromptMessages(
127
125
  this.repairPromptFactory.create({
@@ -132,10 +130,7 @@ export class AgentStructuredOutputRunner {
132
130
  ),
133
131
  ];
134
132
  const repairResponse = await args.invokeTextModel(repairMessages);
135
- const repairResult = this.tryParseAndValidate<TOutput>(
136
- AgentMessageFactory.extractContent(repairResponse),
137
- args.schema,
138
- );
133
+ const repairResult = this.tryParseAndValidate<TOutput>(repairResponse.text, args.schema);
139
134
  if (repairResult.ok) {
140
135
  return repairResult.value;
141
136
  }
@@ -146,29 +141,27 @@ export class AgentStructuredOutputRunner {
146
141
  );
147
142
  }
148
143
 
149
- private createStructuredOutputModel(
150
- model: LangChainChatModelLike,
151
- chatModelConfig: ChatModelConfig,
152
- schema: ZodSchemaAny,
153
- ): LangChainStructuredOutputModelLike | undefined {
154
- if (!this.supportsNativeStructuredOutput(model)) {
144
+ /**
145
+ * Chooses strict mode for OpenAI chat-model configs, off otherwise. Extendable in future for
146
+ * other providers that adopt the same "supply a JSON Schema record directly" contract.
147
+ */
148
+ private resolveStructuredOutputOptions(chatModelConfig: ChatModelConfig): StructuredOutputOptions | undefined {
149
+ if (chatModelConfig.type !== OpenAIChatModelFactory) {
155
150
  return undefined;
156
151
  }
157
- const options = this.getStructuredOutputOptions(chatModelConfig);
158
- return model.withStructuredOutput(schema, options);
152
+ return { strict: true, schemaName: AgentStructuredOutputRunner.structuredOutputSchemaName };
159
153
  }
160
154
 
161
- private getStructuredOutputOptions(chatModelConfig: ChatModelConfig): ChatModelStructuredOutputOptions | undefined {
162
- return this.openAiStructuredOutputMethodFactory.create(chatModelConfig) ?? { strict: true };
163
- }
164
-
165
- private supportsNativeStructuredOutput(model: LangChainChatModelLike): model is LangChainChatModelLike & {
166
- withStructuredOutput: (
167
- outputSchema: ZodSchemaAny,
168
- config?: ChatModelStructuredOutputOptions,
169
- ) => LangChainStructuredOutputModelLike;
170
- } {
171
- return typeof model.withStructuredOutput === "function";
155
+ private resolveOutputSchemaForModel(
156
+ schema: ZodSchemaAny,
157
+ options: StructuredOutputOptions | undefined,
158
+ ): StructuredOutputSchemaForModel {
159
+ if (!options?.strict) {
160
+ return schema;
161
+ }
162
+ return this.openAiStrictJsonSchemaFactory.createStructuredOutputRecord(schema, {
163
+ schemaName: options.schemaName ?? AgentStructuredOutputRunner.structuredOutputSchemaName,
164
+ });
172
165
  }
173
166
 
174
167
  private tryParseAndValidate<TOutput>(content: string, schema: ZodSchemaAny): ParsedStructuredOutputResult<TOutput> {
@@ -214,7 +207,8 @@ export class AgentStructuredOutputRunner {
214
207
 
215
208
  private toJson(value: unknown): string {
216
209
  try {
217
- return JSON.stringify(value);
210
+ const serialized = JSON.stringify(value);
211
+ return serialized ?? String(value);
218
212
  } catch (error) {
219
213
  return `<<unserializable: ${this.summarizeError(error)}>>`;
220
214
  }
@@ -1,5 +1,5 @@
1
1
  import type { JsonValue, NodeExecutionContext } from "@codemation/core";
2
- import { CodemationTelemetryAttributeNames, ConnectionInvocationIdFactory, inject, injectable } from "@codemation/core";
2
+ import { CodemationTelemetryAttributeNames, inject, injectable } from "@codemation/core";
3
3
 
4
4
  import type { AIAgent } from "./AIAgentConfig";
5
5
  import { AgentOutputFactory } from "./AgentOutputFactory";
@@ -53,7 +53,7 @@ export class AgentToolExecutionCoordinator {
53
53
  ): Promise<ExecutedToolCall> {
54
54
  const { plannedToolCall, ctx } = args;
55
55
  const toolCallInputsByPort = AgentToolCallPortMap.fromInput(plannedToolCall.toolCall.input ?? {});
56
- const invocationId = ConnectionInvocationIdFactory.create();
56
+ const invocationId = plannedToolCall.invocationId;
57
57
  const startedAt = new Date();
58
58
  const span = ctx.telemetry.startChildSpan({
59
59
  name: "agent.tool.call",
@@ -62,6 +62,13 @@ export class AgentToolExecutionCoordinator {
62
62
  attributes: {
63
63
  [CodemationTelemetryAttributeNames.connectionInvocationId]: invocationId,
64
64
  [CodemationTelemetryAttributeNames.toolName]: plannedToolCall.binding.config.name,
65
+ ...(ctx.iterationId ? { [CodemationTelemetryAttributeNames.iterationId]: ctx.iterationId } : {}),
66
+ ...(typeof ctx.itemIndex === "number"
67
+ ? { [CodemationTelemetryAttributeNames.iterationIndex]: ctx.itemIndex }
68
+ : {}),
69
+ ...(ctx.parentInvocationId
70
+ ? { [CodemationTelemetryAttributeNames.parentInvocationId]: ctx.parentInvocationId }
71
+ : {}),
65
72
  },
66
73
  });
67
74
  await ctx.nodeState?.markRunning({
@@ -69,10 +76,25 @@ export class AgentToolExecutionCoordinator {
69
76
  activationId: ctx.activationId,
70
77
  inputsByPort: toolCallInputsByPort,
71
78
  });
79
+ await ctx.nodeState?.appendConnectionInvocation({
80
+ invocationId,
81
+ connectionNodeId: plannedToolCall.nodeId,
82
+ parentAgentNodeId: ctx.nodeId,
83
+ parentAgentActivationId: ctx.activationId,
84
+ status: "running",
85
+ managedInput: this.toJsonValue(plannedToolCall.toolCall.input),
86
+ queuedAt: startedAt.toISOString(),
87
+ startedAt: startedAt.toISOString(),
88
+ iterationId: ctx.iterationId,
89
+ parentInvocationId: ctx.parentInvocationId,
90
+ });
72
91
 
73
92
  try {
74
- const serialized = await plannedToolCall.binding.langChainTool.invoke(plannedToolCall.toolCall.input ?? {});
75
- const result = this.parseToolOutput(serialized);
93
+ const result = await plannedToolCall.binding.execute(plannedToolCall.toolCall.input ?? {}, {
94
+ parentSpan: span,
95
+ parentInvocationId: invocationId,
96
+ });
97
+ const serialized = typeof result === "string" ? result : JSON.stringify(result);
76
98
  const finishedAt = new Date();
77
99
  await ctx.nodeState?.markCompleted({
78
100
  nodeId: plannedToolCall.nodeId,
@@ -102,6 +124,8 @@ export class AgentToolExecutionCoordinator {
102
124
  queuedAt: startedAt.toISOString(),
103
125
  startedAt: startedAt.toISOString(),
104
126
  finishedAt: finishedAt.toISOString(),
127
+ iterationId: ctx.iterationId,
128
+ parentInvocationId: ctx.parentInvocationId,
105
129
  });
106
130
  return {
107
131
  toolName: plannedToolCall.binding.config.name,
@@ -113,7 +137,7 @@ export class AgentToolExecutionCoordinator {
113
137
  const classification = this.errorClassifier.classify({
114
138
  error,
115
139
  toolName: plannedToolCall.binding.config.name,
116
- schema: plannedToolCall.binding.langChainTool.schema,
140
+ schema: plannedToolCall.binding.inputSchema,
117
141
  });
118
142
 
119
143
  if (classification.kind !== "repairable_validation_error") {
@@ -262,6 +286,8 @@ export class AgentToolExecutionCoordinator {
262
286
  queuedAt: args.startedAt.toISOString(),
263
287
  startedAt: args.startedAt.toISOString(),
264
288
  finishedAt: finishedAt.toISOString(),
289
+ iterationId: args.ctx.iterationId,
290
+ parentInvocationId: args.ctx.parentInvocationId,
265
291
  });
266
292
  }
267
293
 
@@ -324,17 +350,6 @@ export class AgentToolExecutionCoordinator {
324
350
  return `Your previous tool call for "${toolName}" was invalid because field "${fieldPath}" failed validation: ${firstIssue.message}`;
325
351
  }
326
352
 
327
- private parseToolOutput(serialized: unknown): unknown {
328
- if (typeof serialized !== "string") {
329
- return serialized;
330
- }
331
- try {
332
- return JSON.parse(serialized) as unknown;
333
- } catch {
334
- return serialized;
335
- }
336
- }
337
-
338
353
  private toJsonValue(value: unknown): JsonValue | undefined {
339
354
  if (value === undefined) {
340
355
  return undefined;
@@ -0,0 +1,42 @@
1
+ import type { AssertionResult, RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
2
+ import { node } from "@codemation/core";
3
+
4
+ import { Assertion } from "./assertion";
5
+
6
+ /**
7
+ * Runs the author's `assertions` callback for each input item and emits one workflow `Item` per
8
+ * returned {@link AssertionResult} on `main`. Persistence is handled by a host-side subscriber
9
+ * to `nodeCompleted` events that filters on `config.emitsAssertions === true`; this node does
10
+ * not write to any store on its own.
11
+ *
12
+ * If the author callback throws, we emit a single synthetic AssertionResult with `errored: true`
13
+ * and `score: 0`. Without this catch the whole node would fail and no assertion row would be
14
+ * persisted — making the rollup blind to "the assertion code itself is broken." The synthetic
15
+ * row keeps `failedAssertionsByRunId` consistent and gives the UI something to surface.
16
+ */
17
+ @node({ packageName: "@codemation/core-nodes" })
18
+ export class AssertionNode implements RunnableNode<Assertion<any>> {
19
+ kind = "node" as const;
20
+ outputPorts = ["main"] as const;
21
+
22
+ async execute(args: RunnableNodeExecuteArgs<Assertion<any>>): Promise<unknown> {
23
+ const ctx = args.ctx;
24
+ const config = ctx.config;
25
+ try {
26
+ const results: ReadonlyArray<AssertionResult> = await config.assertions(args.item, ctx);
27
+ // Engine "array → fan-out on main, each element is item.json" — returning the plain results
28
+ // makes downstream `item.json` exactly an AssertionResult. Wrapping in `{ json: result }`
29
+ // would double-wrap (engine would see `Item`-shaped values but treat them as JSON values).
30
+ return [...results];
31
+ } catch (err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ const erroredResult: AssertionResult = {
34
+ name: config.name ?? "assertion",
35
+ score: 0,
36
+ errored: true,
37
+ message,
38
+ };
39
+ return [erroredResult];
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,45 @@
1
+ import type { TriggerNodeConfig, TypeToken } from "@codemation/core";
2
+
3
+ import { Cron } from "croner";
4
+ import type { CronCallback } from "croner";
5
+
6
+ import { CronTriggerNode } from "./CronTriggerNode";
7
+
8
+ export type CronTickJson = { firedAt: string; scheduledFor: string };
9
+
10
+ /**
11
+ * Schedules a workflow on a standard cron expression.
12
+ *
13
+ * Each tick emits one item: `{ firedAt: string, scheduledFor: string }` — both ISO-8601 timestamps.
14
+ * `firedAt` is the wall-clock moment the callback ran; `scheduledFor` is the cron-computed
15
+ * firing instant (these differ when the job was delayed).
16
+ *
17
+ * Timezone defaults to UTC when omitted — cron without an explicit TZ is a DST footgun.
18
+ */
19
+ export class CronTrigger implements TriggerNodeConfig<CronTickJson> {
20
+ readonly kind = "trigger" as const;
21
+ readonly type: TypeToken<unknown> = CronTriggerNode;
22
+ readonly icon = "lucide:clock" as const;
23
+ readonly id?: string;
24
+
25
+ constructor(
26
+ public readonly name: string,
27
+ private readonly args: Readonly<{ schedule: string; timezone?: string }>,
28
+ id?: string,
29
+ ) {
30
+ new Cron(args.schedule, { paused: true, timezone: args.timezone });
31
+ this.id = id;
32
+ }
33
+
34
+ get schedule(): string {
35
+ return this.args.schedule;
36
+ }
37
+
38
+ get timezone(): string | undefined {
39
+ return this.args.timezone;
40
+ }
41
+
42
+ createJob(callback: CronCallback): Cron {
43
+ return new Cron(this.args.schedule, { timezone: this.args.timezone, protect: true }, callback);
44
+ }
45
+ }
@@ -0,0 +1,40 @@
1
+ import type {
2
+ Items,
3
+ NodeExecutionContext,
4
+ NodeOutputs,
5
+ TestableTriggerNode,
6
+ TriggerSetupContext,
7
+ TriggerTestItemsContext,
8
+ } from "@codemation/core";
9
+
10
+ import { node } from "@codemation/core";
11
+
12
+ import { CronTrigger } from "./CronTriggerFactory";
13
+
14
+ @node({ packageName: "@codemation/core-nodes" })
15
+ export class CronTriggerNode implements TestableTriggerNode<CronTrigger> {
16
+ readonly kind = "trigger" as const;
17
+ readonly outputPorts = ["main"] as const;
18
+
19
+ async setup(ctx: TriggerSetupContext<CronTrigger>): Promise<undefined> {
20
+ const job = ctx.config.createJob(async (self) => {
21
+ const scheduledFor = self.currentRun()?.toISOString() ?? ctx.now().toISOString();
22
+ await ctx.emit([{ json: { firedAt: ctx.now().toISOString(), scheduledFor } }]);
23
+ });
24
+ ctx.registerCleanup({
25
+ stop: () => {
26
+ job.stop();
27
+ },
28
+ });
29
+ return undefined;
30
+ }
31
+
32
+ async execute(items: Items, _ctx: NodeExecutionContext<CronTrigger>): Promise<NodeOutputs> {
33
+ return { main: items };
34
+ }
35
+
36
+ async getTestItems(ctx: TriggerTestItemsContext<CronTrigger>): Promise<Items> {
37
+ const nowIso = ctx.now().toISOString();
38
+ return [{ json: { firedAt: nowIso, scheduledFor: nowIso } }];
39
+ }
40
+ }