@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.
- package/CHANGELOG.md +222 -0
- package/dist/index.cjs +3485 -474
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1763 -685
- package/dist/index.d.ts +1763 -685
- package/dist/index.js +3452 -479
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/src/authoring/defineRestNode.types.ts +204 -0
- package/src/chatModels/OpenAIChatModelFactory.ts +17 -8
- package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +123 -0
- package/src/credentials/ApiKeyCredentialType.ts +60 -0
- package/src/credentials/BasicAuthCredentialType.ts +51 -0
- package/src/credentials/BearerTokenCredentialType.ts +40 -0
- package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +117 -0
- package/src/credentials/OAuth2TokenExchangeFactory.ts +52 -0
- package/src/credentials/index.ts +4 -0
- package/src/http/HttpBodyBuilder.ts +90 -0
- package/src/http/HttpRequestExecutor.ts +150 -0
- package/src/http/HttpUrlBuilder.ts +22 -0
- package/src/http/httpRequest.types.ts +69 -0
- package/src/index.ts +10 -1
- package/src/nodes/AIAgentExecutionHelpersFactory.ts +45 -59
- package/src/nodes/AIAgentNode.ts +391 -288
- package/src/nodes/AgentMessageFactory.ts +57 -49
- package/src/nodes/AgentStructuredOutputRunner.ts +65 -71
- package/src/nodes/AgentToolExecutionCoordinator.ts +31 -16
- package/src/nodes/AssertionNode.ts +42 -0
- package/src/nodes/CronTriggerFactory.ts +45 -0
- package/src/nodes/CronTriggerNode.ts +40 -0
- package/src/nodes/HttpRequestNodeFactory.ts +99 -23
- package/src/nodes/IsTestRunNode.ts +25 -0
- package/src/nodes/NodeBackedToolRuntime.ts +40 -4
- package/src/nodes/TestTriggerNode.ts +33 -0
- package/src/nodes/WebhookTriggerFactory.ts +1 -1
- package/src/nodes/aggregate.ts +1 -1
- package/src/nodes/aiAgentSupport.types.ts +22 -2
- package/src/nodes/assertion.ts +42 -0
- package/src/nodes/collections/collectionDeleteNode.types.ts +23 -0
- package/src/nodes/collections/collectionFindOneNode.types.ts +26 -0
- package/src/nodes/collections/collectionGetNode.types.ts +26 -0
- package/src/nodes/collections/collectionInsertNode.types.ts +22 -0
- package/src/nodes/collections/collectionListNode.types.ts +30 -0
- package/src/nodes/collections/collectionUpdateNode.types.ts +23 -0
- package/src/nodes/collections/index.ts +6 -0
- package/src/nodes/httpRequest.ts +62 -1
- package/src/nodes/if.ts +1 -1
- package/src/nodes/isTestRun.ts +24 -0
- package/src/nodes/mapData.ts +1 -0
- package/src/nodes/merge.ts +1 -1
- package/src/nodes/noOp.ts +1 -0
- package/src/nodes/split.ts +1 -1
- package/src/nodes/testTrigger.ts +72 -0
- package/src/nodes/wait.ts +1 -0
- package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +0 -46
|
@@ -1,69 +1,77 @@
|
|
|
1
1
|
import type { AgentMessageDto, AgentToolCall } from "@codemation/core";
|
|
2
2
|
|
|
3
|
-
import {
|
|
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<
|
|
13
|
+
static createPromptMessages(messages: ReadonlyArray<AgentMessageDto>): ReadonlyArray<ModelMessage> {
|
|
7
14
|
return messages.map((message) => this.createPromptMessage(message));
|
|
8
15
|
}
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
static
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
57
|
-
|
|
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):
|
|
68
|
+
private static createPromptMessage(message: AgentMessageDto): ModelMessage {
|
|
61
69
|
if (message.role === "system") {
|
|
62
|
-
return
|
|
70
|
+
return { role: "system", content: message.content };
|
|
63
71
|
}
|
|
64
72
|
if (message.role === "assistant") {
|
|
65
|
-
return
|
|
73
|
+
return { role: "assistant", content: message.content };
|
|
66
74
|
}
|
|
67
|
-
return
|
|
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 {
|
|
4
|
+
import type { ModelMessage } from "ai";
|
|
11
5
|
import { ZodError } from "zod";
|
|
12
6
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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(
|
|
38
|
-
private readonly
|
|
48
|
+
@inject(OpenAiStrictJsonSchemaFactory)
|
|
49
|
+
private readonly openAiStrictJsonSchemaFactory: OpenAiStrictJsonSchemaFactory,
|
|
39
50
|
) {}
|
|
40
51
|
|
|
41
52
|
async resolve<TOutput>(
|
|
42
53
|
args: Readonly<{
|
|
43
|
-
model:
|
|
54
|
+
model: ChatLanguageModel;
|
|
44
55
|
chatModelConfig: ChatModelConfig;
|
|
45
56
|
schema: ZodSchemaAny;
|
|
46
|
-
conversation: ReadonlyArray<
|
|
47
|
-
|
|
57
|
+
conversation: ReadonlyArray<ModelMessage>;
|
|
58
|
+
rawFinalText?: string;
|
|
48
59
|
agentName: string;
|
|
49
60
|
nodeId: string;
|
|
50
|
-
invokeTextModel: (messages: ReadonlyArray<
|
|
61
|
+
invokeTextModel: (messages: ReadonlyArray<ModelMessage>) => Promise<{ text: string }>;
|
|
51
62
|
invokeStructuredModel: (
|
|
52
|
-
|
|
53
|
-
messages: ReadonlyArray<
|
|
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.
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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<
|
|
113
|
+
conversation: ReadonlyArray<ModelMessage>;
|
|
116
114
|
lastFailure: ParsedStructuredOutputFailure;
|
|
117
115
|
agentName: string;
|
|
118
116
|
nodeId: string;
|
|
119
|
-
invokeTextModel: (messages: ReadonlyArray<
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
):
|
|
154
|
-
if (
|
|
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
|
-
|
|
158
|
-
return model.withStructuredOutput(schema, options);
|
|
152
|
+
return { strict: true, schemaName: AgentStructuredOutputRunner.structuredOutputSchemaName };
|
|
159
153
|
}
|
|
160
154
|
|
|
161
|
-
private
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
75
|
-
|
|
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.
|
|
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
|
+
}
|