@codemation/core-nodes 0.12.0 → 0.14.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 +30 -0
- package/dist/index.cjs +180 -413
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -1303
- package/dist/index.d.ts +99 -1303
- package/dist/index.js +181 -415
- package/dist/index.js.map +1 -1
- package/dist/metadata.json +1 -1
- package/package.json +2 -2
- package/src/authoring/defineRestNode.types.ts +0 -84
- package/src/canvasIconName.ts +0 -7
- package/src/chatModels/CodemationChatModelConfig.ts +0 -10
- package/src/chatModels/CodemationChatModelFactory.ts +0 -7
- package/src/chatModels/ManagedHmacSignerFactory.types.ts +0 -35
- package/src/chatModels/OpenAIChatModelFactory.ts +0 -2
- package/src/chatModels/OpenAiChatModelPresetsFactory.ts +0 -5
- package/src/chatModels/OpenAiCredentialSession.ts +0 -1
- package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +0 -21
- package/src/credentials/ApiKeyCredentialType.ts +0 -3
- package/src/credentials/BasicAuthCredentialType.ts +0 -4
- package/src/credentials/BearerTokenCredentialType.ts +0 -4
- package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +0 -19
- package/src/credentials/OAuth2TokenExchangeFactory.ts +0 -7
- package/src/http/HttpBodyBuilder.ts +0 -16
- package/src/http/HttpRequestExecutor.ts +0 -35
- package/src/http/HttpUrlBuilder.ts +0 -4
- package/src/http/SSRFBlockedError.ts +0 -4
- package/src/http/SsrfGuard.ts +10 -50
- package/src/http/httpRequest.types.ts +0 -49
- package/src/index.ts +1 -0
- package/src/nodes/AIAgentConfig.ts +3 -39
- package/src/nodes/AIAgentExecutionHelpersFactory.ts +0 -37
- package/src/nodes/AIAgentNode.ts +4 -134
- package/src/nodes/AgentBinaryContentFactory.ts +0 -12
- package/src/nodes/AgentLoopCheckpoint.types.ts +0 -13
- package/src/nodes/AgentMessageFactory.ts +17 -19
- package/src/nodes/AgentStructuredOutputRunner.ts +0 -17
- package/src/nodes/AgentToolExecutionCoordinator.ts +0 -12
- package/src/nodes/AgentToolResultContentFactory.ts +126 -0
- package/src/nodes/AssertionNode.ts +0 -14
- package/src/nodes/BM25Index.ts +0 -14
- package/src/nodes/ConnectionCredentialExecutionContextFactory.ts +0 -5
- package/src/nodes/ConnectionCredentialNode.ts +0 -4
- package/src/nodes/CronTriggerFactory.ts +0 -9
- package/src/nodes/DeferredMetaToolStrategy.ts +0 -18
- package/src/nodes/DeferredMetaToolStrategyFactory.ts +0 -5
- package/src/nodes/HttpRequestNodeFactory.ts +0 -14
- package/src/nodes/InboxApprovalNode.types.ts +0 -16
- package/src/nodes/IsTestRunNode.ts +0 -8
- package/src/nodes/ManualTriggerFactory.ts +0 -3
- package/src/nodes/ManualTriggerNode.ts +0 -4
- package/src/nodes/MergeNode.ts +0 -1
- package/src/nodes/NodeBackedToolRuntime.ts +0 -14
- package/src/nodes/SubWorkflowNode.ts +0 -3
- package/src/nodes/SwitchNode.ts +0 -3
- package/src/nodes/TestTriggerNode.ts +0 -9
- package/src/nodes/aiAgentSupport.types.ts +0 -16
- package/src/nodes/assertion.ts +0 -10
- package/src/nodes/codemationDocumentScannerNode.ts +0 -18
- package/src/nodes/collections/collectionListNode.types.ts +0 -1
- package/src/nodes/httpRequest.ts +0 -68
- package/src/nodes/isTestRun.ts +0 -4
- package/src/nodes/mapData.ts +0 -1
- package/src/nodes/merge.ts +0 -4
- package/src/nodes/mergeExecutionUtils.types.ts +0 -3
- package/src/nodes/nodeOptions.types.ts +0 -8
- package/src/nodes/schedulePollingTrigger.ts +37 -0
- package/src/nodes/split.ts +0 -4
- package/src/nodes/testTrigger.ts +0 -21
- package/src/nodes/wait.ts +0 -1
- package/src/nodes/webhookTriggerNode.ts +0 -5
- package/src/register.types.ts +0 -10
- package/src/workflows/AIAgentConnectionWorkflowExpander.ts +0 -3
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { ToolResultPart } from "ai";
|
|
2
|
+
|
|
3
|
+
type ContentOutputValue = Extract<ToolResultPart["output"], { type: "content" }>["value"];
|
|
4
|
+
type ContentOutputPart = ContentOutputValue[number];
|
|
5
|
+
|
|
6
|
+
const MAX_INLINE_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
const KNOWN_MCP_BLOCK_TYPES: ReadonlySet<string> = new Set(["text", "image", "audio", "resource", "resource_link"]);
|
|
9
|
+
|
|
10
|
+
type McpContentBlock = Readonly<{
|
|
11
|
+
type?: unknown;
|
|
12
|
+
text?: unknown;
|
|
13
|
+
data?: unknown;
|
|
14
|
+
mimeType?: unknown;
|
|
15
|
+
uri?: unknown;
|
|
16
|
+
name?: unknown;
|
|
17
|
+
resource?: Readonly<{ blob?: unknown; text?: unknown; mimeType?: unknown; uri?: unknown; name?: unknown }>;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export class AgentToolResultContentFactory {
|
|
21
|
+
static tryMapToContentOutput(result: unknown): ContentOutputValue | undefined {
|
|
22
|
+
const blocks = AgentToolResultContentFactory.contentBlocks(result);
|
|
23
|
+
if (blocks === undefined) return undefined;
|
|
24
|
+
|
|
25
|
+
const parts: ContentOutputPart[] = [];
|
|
26
|
+
let inlinedBytes = 0;
|
|
27
|
+
for (const block of blocks) {
|
|
28
|
+
const mapped = AgentToolResultContentFactory.mapBlock(block, inlinedBytes);
|
|
29
|
+
parts.push(mapped.part);
|
|
30
|
+
inlinedBytes += mapped.bytes;
|
|
31
|
+
}
|
|
32
|
+
return parts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private static contentBlocks(result: unknown): ReadonlyArray<McpContentBlock> | undefined {
|
|
36
|
+
if (result === null || typeof result !== "object") return undefined;
|
|
37
|
+
const content = (result as { content?: unknown }).content;
|
|
38
|
+
if (!Array.isArray(content) || content.length === 0) return undefined;
|
|
39
|
+
const allTyped = content.every(
|
|
40
|
+
(block) => block !== null && typeof block === "object" && typeof (block as { type?: unknown }).type === "string",
|
|
41
|
+
);
|
|
42
|
+
if (!allTyped) return undefined;
|
|
43
|
+
const looksMcp = content.some((block) => KNOWN_MCP_BLOCK_TYPES.has((block as { type: string }).type));
|
|
44
|
+
return looksMcp ? (content as ReadonlyArray<McpContentBlock>) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private static mapBlock(
|
|
48
|
+
block: McpContentBlock,
|
|
49
|
+
inlinedBytesSoFar: number,
|
|
50
|
+
): Readonly<{ part: ContentOutputPart; bytes: number }> {
|
|
51
|
+
const type = block.type;
|
|
52
|
+
if (type === "text" && typeof block.text === "string") {
|
|
53
|
+
return { part: { type: "text", text: block.text }, bytes: 0 };
|
|
54
|
+
}
|
|
55
|
+
if (type === "image" && typeof block.data === "string" && typeof block.mimeType === "string") {
|
|
56
|
+
return AgentToolResultContentFactory.mapBinary({
|
|
57
|
+
base64: block.data,
|
|
58
|
+
mediaType: block.mimeType,
|
|
59
|
+
inlinedBytesSoFar,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (type === "resource" && block.resource) {
|
|
63
|
+
return AgentToolResultContentFactory.mapEmbeddedResource(block.resource, inlinedBytesSoFar);
|
|
64
|
+
}
|
|
65
|
+
if (type === "resource_link" && typeof block.uri === "string") {
|
|
66
|
+
const mime = typeof block.mimeType === "string" ? ` (${block.mimeType})` : "";
|
|
67
|
+
return { part: { type: "text", text: `[linked resource: ${block.uri}${mime}]` }, bytes: 0 };
|
|
68
|
+
}
|
|
69
|
+
return { part: { type: "text", text: `[unsupported tool content block: ${String(type)}]` }, bytes: 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private static mapEmbeddedResource(
|
|
73
|
+
resource: NonNullable<McpContentBlock["resource"]>,
|
|
74
|
+
inlinedBytesSoFar: number,
|
|
75
|
+
): Readonly<{ part: ContentOutputPart; bytes: number }> {
|
|
76
|
+
if (typeof resource.text === "string") {
|
|
77
|
+
return { part: { type: "text", text: resource.text }, bytes: 0 };
|
|
78
|
+
}
|
|
79
|
+
if (typeof resource.blob === "string" && typeof resource.mimeType === "string") {
|
|
80
|
+
return AgentToolResultContentFactory.mapBinary({
|
|
81
|
+
base64: resource.blob,
|
|
82
|
+
mediaType: resource.mimeType,
|
|
83
|
+
filename: typeof resource.name === "string" ? resource.name : undefined,
|
|
84
|
+
inlinedBytesSoFar,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const uri = typeof resource.uri === "string" ? resource.uri : "unknown";
|
|
88
|
+
return { part: { type: "text", text: `[embedded resource: ${uri}]` }, bytes: 0 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static mapBinary(
|
|
92
|
+
args: Readonly<{ base64: string; mediaType: string; filename?: string; inlinedBytesSoFar: number }>,
|
|
93
|
+
): Readonly<{ part: ContentOutputPart; bytes: number }> {
|
|
94
|
+
const rawBytes = Math.floor((args.base64.length * 3) / 4);
|
|
95
|
+
if (args.inlinedBytesSoFar + rawBytes > MAX_INLINE_BYTES) {
|
|
96
|
+
const name = args.filename ? ` "${args.filename}"` : "";
|
|
97
|
+
const kb = Math.round(rawBytes / 1024);
|
|
98
|
+
return {
|
|
99
|
+
part: {
|
|
100
|
+
type: "text",
|
|
101
|
+
text: `[binary${name} (${args.mediaType}, ~${kb} KB) omitted: exceeds the per-tool-result inline limit]`,
|
|
102
|
+
},
|
|
103
|
+
bytes: 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (args.mediaType.startsWith("image/")) {
|
|
107
|
+
return { part: { type: "image-data", data: args.base64, mediaType: args.mediaType }, bytes: rawBytes };
|
|
108
|
+
}
|
|
109
|
+
if (args.mediaType === "application/pdf") {
|
|
110
|
+
return {
|
|
111
|
+
part: {
|
|
112
|
+
type: "file-data",
|
|
113
|
+
data: args.base64,
|
|
114
|
+
mediaType: args.mediaType,
|
|
115
|
+
...(args.filename ? { filename: args.filename } : {}),
|
|
116
|
+
},
|
|
117
|
+
bytes: rawBytes,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const name = args.filename ? ` "${args.filename}"` : "";
|
|
121
|
+
return {
|
|
122
|
+
part: { type: "text", text: `[binary${name} (${args.mediaType}) not inlined: unsupported by the model]` },
|
|
123
|
+
bytes: 0,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -3,17 +3,6 @@ import { node } from "@codemation/core";
|
|
|
3
3
|
|
|
4
4
|
import { Assertion } from "./assertion";
|
|
5
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
6
|
@node({ packageName: "@codemation/core-nodes" })
|
|
18
7
|
export class AssertionNode implements RunnableNode<Assertion<any>> {
|
|
19
8
|
kind = "node" as const;
|
|
@@ -24,9 +13,6 @@ export class AssertionNode implements RunnableNode<Assertion<any>> {
|
|
|
24
13
|
const config = ctx.config;
|
|
25
14
|
try {
|
|
26
15
|
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
16
|
return [...results];
|
|
31
17
|
} catch (err) {
|
|
32
18
|
const message = err instanceof Error ? err.message : String(err);
|
package/src/nodes/BM25Index.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal BM25 (Okapi BM25) implementation for indexing MCP tool descriptions.
|
|
3
|
-
*
|
|
4
|
-
* Parameters: k1=1.5, b=0.75 (standard defaults).
|
|
5
|
-
* Tokenisation: lowercase, split on non-alphanumerics, filter empties.
|
|
6
|
-
*/
|
|
7
1
|
export class BM25Index {
|
|
8
2
|
private readonly k1 = 1.5;
|
|
9
3
|
private readonly b = 0.75;
|
|
@@ -12,10 +6,6 @@ export class BM25Index {
|
|
|
12
6
|
private readonly df = new Map<string, number>();
|
|
13
7
|
private avgDocLen = 0;
|
|
14
8
|
|
|
15
|
-
/**
|
|
16
|
-
* Add all documents at once. After calling this, search is available.
|
|
17
|
-
* Documents are indexed in insertion order; search returns their indices.
|
|
18
|
-
*/
|
|
19
9
|
add(docs: ReadonlyArray<string>): void {
|
|
20
10
|
const docTerms = docs.map((d) => this.tokenize(d));
|
|
21
11
|
|
|
@@ -34,10 +24,6 @@ export class BM25Index {
|
|
|
34
24
|
this.avgDocLen = docTerms.length > 0 ? totalLen / docTerms.length : 0;
|
|
35
25
|
}
|
|
36
26
|
|
|
37
|
-
/**
|
|
38
|
-
* Returns up to `limit` document indices ranked by BM25 score (highest first).
|
|
39
|
-
* Returns an empty array if the index is empty or the query matches nothing.
|
|
40
|
-
*/
|
|
41
27
|
search(query: string, limit: number): ReadonlyArray<number> {
|
|
42
28
|
const n = this.tf.length;
|
|
43
29
|
if (n === 0) return [];
|
|
@@ -8,11 +8,6 @@ import type {
|
|
|
8
8
|
|
|
9
9
|
import { CredentialResolverFactory } from "@codemation/core/bootstrap";
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Builds a {@link NodeExecutionContext} whose identity for credential binding and `getCredential`
|
|
13
|
-
* is a **connection-owned** workflow node id (`ConnectionNodeIdFactory` in `@codemation/core`),
|
|
14
|
-
* not the executing parent node. Use for LLM slots, tool slots, or any connection-scoped owner.
|
|
15
|
-
*/
|
|
16
11
|
export class ConnectionCredentialExecutionContextFactory {
|
|
17
12
|
private readonly credentialResolverFactory: CredentialResolverFactory;
|
|
18
13
|
|
|
@@ -3,10 +3,6 @@ import { node } from "@codemation/core";
|
|
|
3
3
|
|
|
4
4
|
import type { ConnectionCredentialNodeConfig } from "./ConnectionCredentialNodeConfig";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Placeholder runnable node for connection-owned workflow nodes (LLM/tool slots).
|
|
8
|
-
* The engine does not schedule these; they exist for credentials, tokens, and UI identity.
|
|
9
|
-
*/
|
|
10
6
|
@node({ packageName: "@codemation/core-nodes" })
|
|
11
7
|
export class ConnectionCredentialNode implements RunnableNode<ConnectionCredentialNodeConfig> {
|
|
12
8
|
kind = "node" as const;
|
|
@@ -8,15 +8,6 @@ import type { NodeBaseOptions } from "./nodeOptions.types";
|
|
|
8
8
|
|
|
9
9
|
export type CronTickJson = { firedAt: string; scheduledFor: string };
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Schedules a workflow on a standard cron expression.
|
|
13
|
-
*
|
|
14
|
-
* Each tick emits one item: `{ firedAt: string, scheduledFor: string }` — both ISO-8601 timestamps.
|
|
15
|
-
* `firedAt` is the wall-clock moment the callback ran; `scheduledFor` is the cron-computed
|
|
16
|
-
* firing instant (these differ when the job was delayed).
|
|
17
|
-
*
|
|
18
|
-
* Timezone defaults to UTC when omitted — cron without an explicit TZ is a DST footgun.
|
|
19
|
-
*/
|
|
20
11
|
export class CronTrigger implements TriggerNodeConfig<CronTickJson> {
|
|
21
12
|
readonly kind = "trigger" as const;
|
|
22
13
|
readonly type: TypeToken<unknown> = CronTriggerNode;
|
|
@@ -21,26 +21,12 @@ interface McpToolEntry {
|
|
|
21
21
|
readonly toolDef: ToolSet[string];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* Default tool-loading strategy: BM25-indexed MCP tool deferral via a `find_tools` meta-tool.
|
|
26
|
-
*
|
|
27
|
-
* - Node-backed tools and pinned MCP tools are always included in every turn.
|
|
28
|
-
* - `find_tools(query, limit?)` is added to the tool set when MCP tools are indexed.
|
|
29
|
-
* - Tools surfaced by `find_tools` are included in subsequent turns.
|
|
30
|
-
*
|
|
31
|
-
* Not DI-managed; instantiated per agent execution by DeferredMetaToolStrategyFactory.
|
|
32
|
-
*/
|
|
33
24
|
export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
|
|
34
25
|
private nodeBackedTools: ToolSet = {};
|
|
35
26
|
private pinnedTools: ToolSet = {};
|
|
36
27
|
private mcpEntries: McpToolEntry[] = [];
|
|
37
28
|
private toolsByServerId = new Map<string, Map<string, ToolSet[string]>>();
|
|
38
29
|
private foundToolIds = new Set<string>();
|
|
39
|
-
/**
|
|
40
|
-
* `jsonSchema` from the `ai` SDK, loaded lazily in {@link initialize} so the SDK
|
|
41
|
-
* (~28MB RSS) stays off the boot path. `initialize` always runs before the sync
|
|
42
|
-
* `getToolsForTurn` → `buildFindToolsDefinition` path, so this is set before use.
|
|
43
|
-
*/
|
|
44
30
|
private jsonSchema!: typeof import("ai").jsonSchema;
|
|
45
31
|
|
|
46
32
|
constructor(
|
|
@@ -124,8 +110,6 @@ export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
|
|
|
124
110
|
|
|
125
111
|
ownsToolName(toolName: string): boolean {
|
|
126
112
|
if (toolName === FIND_TOOLS_NAME) return true;
|
|
127
|
-
// Any tool that came from an MCP server is strategy-owned so the coordinator
|
|
128
|
-
// does not attempt to dispatch it as a node-backed tool.
|
|
129
113
|
for (const serverMap of this.toolsByServerId.values()) {
|
|
130
114
|
if (serverMap.has(toolName)) return true;
|
|
131
115
|
}
|
|
@@ -149,8 +133,6 @@ export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
|
|
|
149
133
|
return results;
|
|
150
134
|
}
|
|
151
135
|
|
|
152
|
-
// Route to the MCP tool's execute callback (injected by AgentMcpIntegrationImpl with
|
|
153
|
-
// telemetry + 403 detection wrapping).
|
|
154
136
|
for (const serverMap of this.toolsByServerId.values()) {
|
|
155
137
|
const toolDef = serverMap.get(toolName);
|
|
156
138
|
if (toolDef) {
|
|
@@ -3,11 +3,6 @@ import { BM25Index } from "./BM25Index";
|
|
|
3
3
|
import { DeferredMetaToolStrategy } from "./DeferredMetaToolStrategy";
|
|
4
4
|
import type { ToolLoadingStrategy, ToolLoadingStrategyInitInput } from "./ToolLoadingStrategy";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Factory for creating and initializing a DeferredMetaToolStrategy per agent execution.
|
|
8
|
-
* Injected into AIAgentNode; each agent call creates its own initialized strategy instance.
|
|
9
|
-
* BM25Index is constructed here (this file is a composition root via the Factory suffix).
|
|
10
|
-
*/
|
|
11
6
|
@injectable()
|
|
12
7
|
export class DeferredMetaToolStrategyFactory {
|
|
13
8
|
async create(input: ToolLoadingStrategyInitInput): Promise<ToolLoadingStrategy> {
|
|
@@ -39,9 +39,6 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
|
|
|
39
39
|
ctx: ctx as unknown as HttpRequestSpec["ctx"],
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
// Build the request (headers, body encoding, URL query merge) once,
|
|
43
|
-
// then make a SINGLE fetch call and decide what to do with the response.
|
|
44
|
-
// This avoids a double-fetch regression for auto-mode binary responses.
|
|
45
42
|
const executor = new HttpRequestExecutor(
|
|
46
43
|
globalThis.fetch,
|
|
47
44
|
new HttpBodyBuilder(),
|
|
@@ -55,7 +52,6 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
|
|
|
55
52
|
const headers = this.readHeaders(response.headers);
|
|
56
53
|
const mimeType = this.resolveMimeType(headers);
|
|
57
54
|
|
|
58
|
-
// New explicit responseFormat="binary" path — takes precedence over downloadMode.
|
|
59
55
|
if (ctx.config.responseFormat === "binary") {
|
|
60
56
|
return await this.handleBinaryResponse(response, resolvedUrl, headers, mimeType, ctx);
|
|
61
57
|
}
|
|
@@ -90,7 +86,6 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
|
|
|
90
86
|
return outputItem;
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
// Non-binary path: parse JSON or read text.
|
|
94
89
|
const isJson = this.isJsonMimeType(mimeType);
|
|
95
90
|
let json: unknown | undefined;
|
|
96
91
|
let text: string | undefined;
|
|
@@ -130,7 +125,6 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
|
|
|
130
125
|
const slotName = ctx.config.responseBinarySlot;
|
|
131
126
|
const sizeCap = ctx.config.responseSizeCapBytes;
|
|
132
127
|
|
|
133
|
-
// Check Content-Length against size cap before allocating.
|
|
134
128
|
const contentLengthHeader = headers["content-length"];
|
|
135
129
|
if (contentLengthHeader) {
|
|
136
130
|
const declaredSize = parseInt(contentLengthHeader, 10);
|
|
@@ -144,11 +138,6 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
|
|
|
144
138
|
|
|
145
139
|
const filename = this.resolveFilename(resolvedUrl, headers);
|
|
146
140
|
|
|
147
|
-
// Stream response.body straight into binary storage — never load the
|
|
148
|
-
// whole payload into memory. ctx.binary.attach accepts ReadableStream
|
|
149
|
-
// natively. Falls back to arrayBuffer only when response.body is null
|
|
150
|
-
// (rare; 204/304-style responses where the cap-check above already
|
|
151
|
-
// covers the meaningful size case).
|
|
152
141
|
const attachment = await ctx.binary.attach({
|
|
153
142
|
name: slotName,
|
|
154
143
|
body: response.body
|
|
@@ -168,7 +157,6 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
|
|
|
168
157
|
headers,
|
|
169
158
|
binarySlot: slotName,
|
|
170
159
|
contentType: mimeType,
|
|
171
|
-
// Reported by the binary storage adapter after streaming completes.
|
|
172
160
|
size: attachment.size,
|
|
173
161
|
...(filename !== undefined ? { filename } : {}),
|
|
174
162
|
};
|
|
@@ -189,13 +177,11 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
|
|
|
189
177
|
try {
|
|
190
178
|
return await ctx.getCredential<CredentialSession>(slotKey);
|
|
191
179
|
} catch {
|
|
192
|
-
// Credential slot configured but not bound — treat as no credential.
|
|
193
180
|
return undefined;
|
|
194
181
|
}
|
|
195
182
|
}
|
|
196
183
|
|
|
197
184
|
private resolveUrl(item: Item, ctx: NodeExecutionContext<HttpRequest<any, any>>): string {
|
|
198
|
-
// Literal URL in args takes precedence over the legacy urlField approach.
|
|
199
185
|
const literalUrl = ctx.config.args.url;
|
|
200
186
|
if (literalUrl && literalUrl.trim().length > 0) {
|
|
201
187
|
return literalUrl.trim();
|
|
@@ -3,28 +3,12 @@ import { defineHumanApprovalNode } from "@codemation/core";
|
|
|
3
3
|
import type { Item, JsonValue } from "@codemation/core";
|
|
4
4
|
import { InboxChannelResolverToken } from "@codemation/core";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* A subject field (title / body) for an inbox approval. Either a static string
|
|
8
|
-
* or a contextual callback that builds the string from the item using ordinary
|
|
9
|
-
* JavaScript template literals — e.g. `({ item }) => `Approve ${item.json.vendor}``.
|
|
10
|
-
* Code-first: no template DSL, just functions.
|
|
11
|
-
*/
|
|
12
6
|
type InboxSubjectField = string | ((args: { item: Item }) => string);
|
|
13
7
|
|
|
14
8
|
function resolveSubjectField(field: InboxSubjectField, item: Item): string {
|
|
15
9
|
return typeof field === "function" ? field({ item }) : field;
|
|
16
10
|
}
|
|
17
11
|
|
|
18
|
-
/**
|
|
19
|
-
* Auto-detecting inbox approval node.
|
|
20
|
-
*
|
|
21
|
-
* Uses `ctx.resolve(InboxChannelResolverToken)` to pick the right inbox channel
|
|
22
|
-
* at runtime:
|
|
23
|
-
* - In managed mode (PairingConfig present): routes to the control-plane inbox.
|
|
24
|
-
* - Otherwise: routes to the local inbox.
|
|
25
|
-
*
|
|
26
|
-
* Authors use this node directly; no extra wiring needed per deployment mode.
|
|
27
|
-
*/
|
|
28
12
|
export const inboxApproval = defineHumanApprovalNode({
|
|
29
13
|
key: "inbox.approval",
|
|
30
14
|
title: "Inbox Approval",
|
|
@@ -3,14 +3,6 @@ import { emitPorts, node } from "@codemation/core";
|
|
|
3
3
|
|
|
4
4
|
import { IsTestRun } from "./isTestRun";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Routes each item to the `true` port if `ctx.testContext` is set (the run was started by the
|
|
8
|
-
* TestSuiteOrchestrator), else to `false`. Lets workflow authors guard real side-effects:
|
|
9
|
-
*
|
|
10
|
-
* GmailTrigger / TestTrigger → ClassifyAgent → IsTestRun
|
|
11
|
-
* ├── true → AssertionNode
|
|
12
|
-
* └── false → SendReply
|
|
13
|
-
*/
|
|
14
6
|
@node({ packageName: "@codemation/core-nodes" })
|
|
15
7
|
export class IsTestRunNode implements RunnableNode<IsTestRun<unknown>> {
|
|
16
8
|
kind = "node" as const;
|
|
@@ -15,7 +15,6 @@ export class ManualTrigger<TOutputJson = unknown> implements TriggerNodeConfig<T
|
|
|
15
15
|
readonly defaultItems?: Items<TOutputJson>;
|
|
16
16
|
readonly id?: string;
|
|
17
17
|
readonly description?: string;
|
|
18
|
-
/** Manual runs often emit an empty batch; still schedule downstream by default. */
|
|
19
18
|
readonly continueWhenEmptyOutput = true as const;
|
|
20
19
|
|
|
21
20
|
constructor(name?: string, idOrOptions?: string | NodeBaseOptions);
|
|
@@ -29,8 +28,6 @@ export class ManualTrigger<TOutputJson = unknown> implements TriggerNodeConfig<T
|
|
|
29
28
|
defaultItemsOrId?: ManualTriggerDefaultValue<TOutputJson> | string,
|
|
30
29
|
idOrOptions?: string | NodeBaseOptions,
|
|
31
30
|
) {
|
|
32
|
-
// Position 2 keeps its existing string-vs-object meaning (string→id, object/array→default items).
|
|
33
|
-
// Options (id/description) live in the trailing slot, or position 2 when it's a bare string id.
|
|
34
31
|
this.defaultItems = ManualTrigger.resolveDefaultItems(defaultItemsOrId);
|
|
35
32
|
const trailing = idOrOptions ?? (typeof defaultItemsOrId === "string" ? defaultItemsOrId : undefined);
|
|
36
33
|
const options = typeof trailing === "string" ? { id: trailing } : (trailing ?? {});
|
|
@@ -11,10 +11,6 @@ import { node } from "@codemation/core";
|
|
|
11
11
|
|
|
12
12
|
import { ManualTrigger } from "./ManualTriggerFactory";
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* Setup is intentionally a no-op: the engine host can run workflows manually
|
|
16
|
-
* by calling `engine.runWorkflow(workflow, triggerNodeId, items)`.
|
|
17
|
-
*/
|
|
18
14
|
@node({ packageName: "@codemation/core-nodes" })
|
|
19
15
|
export class ManualTriggerNode implements TestableTriggerNode<ManualTrigger<any>> {
|
|
20
16
|
kind = "trigger" as const;
|
package/src/nodes/MergeNode.ts
CHANGED
|
@@ -48,7 +48,6 @@ export class MergeNode implements MultiInputNode<Merge<any, any>> {
|
|
|
48
48
|
return { main: out };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// passThrough (default): for each origin index, take first available input (deterministic input precedence).
|
|
52
51
|
const chosenByOrigin = new Map<number, Item>();
|
|
53
52
|
const fallback: Item[] = [];
|
|
54
53
|
|
|
@@ -63,20 +63,6 @@ export class NodeBackedToolRuntime {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
* Returns a re-rooted child ctx for nested-agent tools (so their LLM/tool connection ids derive
|
|
68
|
-
* from the tool connection node, telemetry parents under the tool-call span, and connection
|
|
69
|
-
* invocations carry `parentInvocationId`). Plain runnable tools (non-agent) keep the orchestrator
|
|
70
|
-
* ctx with only `config` swapped — no nesting concern.
|
|
71
|
-
*
|
|
72
|
-
* The caller (`AIAgentNode.createItemScopedTools`) already wraps the orchestrator ctx via
|
|
73
|
-
* `ConnectionCredentialExecutionContextFactory.forConnectionNode`, so `args.ctx.nodeId` is the
|
|
74
|
-
* tool's own connection node id (e.g. `AIAgentNode:2__conn__tool__searchInMail`). We pass that
|
|
75
|
-
* through as the sub-agent's `nodeId`; deriving another `toolConnectionNodeId(args.ctx.nodeId,
|
|
76
|
-
* config.name)` here would prepend a duplicate `__conn__tool__<name>` segment and exponentially
|
|
77
|
-
* deepen ids on each invocation, which also breaks credential resolution because user-provided
|
|
78
|
-
* bindings sit on the single-level connection node id.
|
|
79
|
-
*/
|
|
80
66
|
private resolveNodeCtx(
|
|
81
67
|
config: NodeBackedToolConfig<any, ZodSchemaAny, ZodSchemaAny>,
|
|
82
68
|
args: ToolExecuteArgs,
|
|
@@ -37,9 +37,6 @@ export class SubWorkflowNode implements RunnableNode<SubWorkflow<any, any>> {
|
|
|
37
37
|
engineMaxSubworkflowDepth: args.ctx.engineMaxSubworkflowDepth,
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
|
-
// Annotate the parent node's snapshot with the child run id so the UI can deep-link to
|
|
41
|
-
// the specific child execution. The engine's subsequent markCompleted preserves this via
|
|
42
|
-
// NodeExecutionSnapshotFactory.completed which carries forward previous.childRunId.
|
|
43
40
|
await args.ctx.nodeState?.setChildRunId?.({ nodeId: args.ctx.nodeId, childRunId: result.runId });
|
|
44
41
|
if (result.status !== "completed") {
|
|
45
42
|
throw new Error(`Subworkflow ${args.ctx.config.workflowId} did not complete (status=${result.status})`);
|
package/src/nodes/SwitchNode.ts
CHANGED
|
@@ -4,9 +4,6 @@ import { emitPorts, node } from "@codemation/core";
|
|
|
4
4
|
import type { Switch } from "./switch";
|
|
5
5
|
import { tagItemForRouterFanIn } from "./mergeExecutionUtils.types";
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Routes each item to exactly one output port. Port names must match workflow edges (see {@link Switch} config).
|
|
9
|
-
*/
|
|
10
7
|
@node({ packageName: "@codemation/core-nodes" })
|
|
11
8
|
export class SwitchNode implements RunnableNode<Switch<any>> {
|
|
12
9
|
kind = "node" as const;
|
|
@@ -9,15 +9,6 @@ import type {
|
|
|
9
9
|
|
|
10
10
|
import { node } from "@codemation/core";
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Author-defined test-fixture trigger. Live activation skips this trigger (filtered by
|
|
14
|
-
* `triggerKind === "test"` in `TriggerRuntimeService`); the `TestSuiteOrchestrator` drives its
|
|
15
|
-
* `generateItems` callback during a TestSuiteRun and dispatches one workflow run per yielded item.
|
|
16
|
-
*
|
|
17
|
-
* `setup` is intentionally a no-op for symmetry with other trigger nodes — the real work happens
|
|
18
|
-
* in the orchestrator. `execute` is a passthrough so items provided to `engine.runWorkflow(...)`
|
|
19
|
-
* (one per case) flow downstream unchanged on `main`.
|
|
20
|
-
*/
|
|
21
12
|
@node({ packageName: "@codemation/core-nodes" })
|
|
22
13
|
export class TestTriggerNode implements TriggerNode<TestTriggerNodeConfig<any>> {
|
|
23
14
|
kind = "trigger" as const;
|
|
@@ -24,30 +24,15 @@ export type ResolvedTool = Readonly<{
|
|
|
24
24
|
}>;
|
|
25
25
|
}>;
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* Per-item binding of a tool: the user config plus the resolved runtime and a snapshot of the
|
|
29
|
-
* original Zod `inputSchema`.
|
|
30
|
-
*
|
|
31
|
-
* `execute` accepts optional `hooks` so the agent coordinator can pass the live `agent.tool.call`
|
|
32
|
-
* span and the planned tool-call's `invocationId`. Node-backed sub-agent tools use these hooks
|
|
33
|
-
* via {@link ChildExecutionScopeFactory} to re-root their runtime ctx under the tool-call boundary
|
|
34
|
-
* (fresh activationId, telemetry parented at the tool-call span, `parentInvocationId` set).
|
|
35
|
-
*
|
|
36
|
-
* `humanApproval` is present only when the tool was created via `defineHumanApprovalNode`
|
|
37
|
-
* (via its marker) — detected during `resolveTools` in `AIAgentNode`.
|
|
38
|
-
*/
|
|
39
27
|
export type ItemScopedToolBinding = Readonly<{
|
|
40
28
|
config: ToolConfig;
|
|
41
29
|
inputSchema: ZodSchemaAny;
|
|
42
30
|
execute(input: unknown, hooks?: ItemScopedToolCallHooks): Promise<unknown>;
|
|
43
|
-
/** Present when this binding is backed by a HITL-approval node (story 10). */
|
|
44
31
|
humanApproval?: Readonly<{ onRejected: "halt" | "return" }>;
|
|
45
32
|
}>;
|
|
46
33
|
|
|
47
34
|
export type ItemScopedToolCallHooks = Readonly<{
|
|
48
|
-
/** Live agent.tool.call span (used to parent sub-agent telemetry). */
|
|
49
35
|
parentSpan?: TelemetrySpanScope;
|
|
50
|
-
/** invocationId of the parent tool call (used to thread `parentInvocationId` through ctx). */
|
|
51
36
|
parentInvocationId?: ConnectionInvocationId;
|
|
52
37
|
}>;
|
|
53
38
|
|
|
@@ -56,7 +41,6 @@ export type PlannedToolCall = Readonly<{
|
|
|
56
41
|
toolCall: AgentToolCall;
|
|
57
42
|
invocationIndex: number;
|
|
58
43
|
nodeId: string;
|
|
59
|
-
/** Stable id reused across queued / running / completed connection invocation rows for this tool call. */
|
|
60
44
|
invocationId: string;
|
|
61
45
|
}>;
|
|
62
46
|
|
package/src/nodes/assertion.ts
CHANGED
|
@@ -14,22 +14,12 @@ export interface AssertionOptions<TInputJson> {
|
|
|
14
14
|
readonly id?: string;
|
|
15
15
|
readonly icon?: string;
|
|
16
16
|
readonly description?: string;
|
|
17
|
-
/**
|
|
18
|
-
* Author callback. Returns one or more {@link AssertionResult}s per input item. Each becomes
|
|
19
|
-
* one emitted output item — useful for per-row reporting in the Tests tab. Return `[]` to
|
|
20
|
-
* emit nothing for this case (rare; usually you want at least a "no-op" pass).
|
|
21
|
-
*/
|
|
22
17
|
assertions(
|
|
23
18
|
item: Item<TInputJson>,
|
|
24
19
|
ctx: NodeExecutionContext<Assertion<TInputJson>>,
|
|
25
20
|
): Promise<ReadonlyArray<AssertionResult>> | ReadonlyArray<AssertionResult>;
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
/**
|
|
29
|
-
* Generic assertion node — the "callback" form. For declarative shorthands (StringEquals,
|
|
30
|
-
* JudgeByAgent) compose this with helpers added in later phases. Sets `emitsAssertions: true`
|
|
31
|
-
* so host-side persisters know to record its outputs as `TestAssertion` rows.
|
|
32
|
-
*/
|
|
33
23
|
export class Assertion<TInputJson = unknown> implements RunnableNodeConfig<TInputJson, AssertionResult> {
|
|
34
24
|
readonly kind = "node" as const;
|
|
35
25
|
readonly type: TypeToken<unknown> = AssertionNode;
|
|
@@ -5,30 +5,21 @@ import { managedHmacFetchFactory } from "../chatModels/ManagedHmacSignerFactory.
|
|
|
5
5
|
const ANALYZER_TYPES = ["document", "invoice", "image", "auto"] as const;
|
|
6
6
|
export type DocScannerAnalyzerType = (typeof ANALYZER_TYPES)[number];
|
|
7
7
|
|
|
8
|
-
/** Per-field value/confidence shape as returned by apps/doc-scanner. */
|
|
9
8
|
export type DocScannerField = Readonly<{
|
|
10
9
|
value: unknown;
|
|
11
10
|
confidence: number | null;
|
|
12
11
|
}>;
|
|
13
12
|
|
|
14
|
-
/** Output shape of CodemationDocumentScanner — identical to the service wire response. */
|
|
15
13
|
export type DocScannerOutput = Readonly<{
|
|
16
14
|
markdown: string;
|
|
17
15
|
fields: Readonly<Record<string, DocScannerField>>;
|
|
18
16
|
}>;
|
|
19
17
|
|
|
20
18
|
export type CodemationDocumentScannerConfig = Readonly<{
|
|
21
|
-
/** Key on `item.binary` that holds the document. Default: "data". */
|
|
22
19
|
binaryField?: string;
|
|
23
|
-
/** Analyzer type. Default: "auto" (routes on mime type on the service side). */
|
|
24
20
|
analyzerType?: DocScannerAnalyzerType;
|
|
25
|
-
/** MIME type override. Falls back to attachment.mimeType. */
|
|
26
21
|
contentType?: string;
|
|
27
|
-
/** Include per-field confidence scores (0–1). Default: false.
|
|
28
|
-
* Enabling this roughly doubles contextualization tokens for document analyzers.
|
|
29
|
-
* Image and auto-to-image requests silently ignore this flag (confidence stays null). */
|
|
30
22
|
includeConfidence?: boolean;
|
|
31
|
-
/** Max bytes checked before any read. Default: 50 MiB (same cap as OCR nodes, LD10). */
|
|
32
23
|
maxBytes?: number;
|
|
33
24
|
}>;
|
|
34
25
|
|
|
@@ -54,9 +45,6 @@ export const codemationDocumentScannerNode = defineNode({
|
|
|
54
45
|
includeConfidence: z.boolean().optional(),
|
|
55
46
|
maxBytes: z.number().int().positive().optional(),
|
|
56
47
|
}),
|
|
57
|
-
// keepBinaries is omitted (false) — the output replaces the item payload.
|
|
58
|
-
// To carry the source binary forward, set keepBinaries: true at the call site
|
|
59
|
-
// or use a downstream step that reads item.binary.
|
|
60
48
|
inspectorSummary({ config }) {
|
|
61
49
|
const cfg = config as unknown as CodemationDocumentScannerConfig;
|
|
62
50
|
const rows: Array<{ label: string; value: string }> = [
|
|
@@ -79,9 +67,6 @@ export const codemationDocumentScannerNode = defineNode({
|
|
|
79
67
|
);
|
|
80
68
|
}
|
|
81
69
|
|
|
82
|
-
// Workspace pairing identity — injected by the CP provisioner (mirrors
|
|
83
|
-
// CodemationChatModelFactory). Passed to the signer; the HMAC binds THIS
|
|
84
|
-
// workspace (LD4) and does not sign the file body (LD11).
|
|
85
70
|
// eslint-disable-next-line no-restricted-properties -- WORKSPACE_ID is injected by the provisioner; read at execute time.
|
|
86
71
|
const workspaceId = process.env["WORKSPACE_ID"];
|
|
87
72
|
// eslint-disable-next-line no-restricted-properties -- WORKSPACE_PAIRING_SECRET is injected by the provisioner; read at execute time.
|
|
@@ -104,9 +89,6 @@ export const codemationDocumentScannerNode = defineNode({
|
|
|
104
89
|
const confidenceSuffix = includeConfidence ? "&confidence=true" : "";
|
|
105
90
|
const url = `${gatewayUrl}/analyze?type=${encodeURIComponent(analyzerType)}${confidenceSuffix}`;
|
|
106
91
|
|
|
107
|
-
// signBody: false (LD11): the HMAC binds the workspace, not the file body.
|
|
108
|
-
// The signer forwards the bytes untouched and signs an empty-body hash, so
|
|
109
|
-
// we never re-read or normalise the binary payload.
|
|
110
92
|
const hmacFetch = managedHmacFetchFactory(workspaceId, pairingSecret, { signBody: false });
|
|
111
93
|
|
|
112
94
|
const response = await hmacFetch(url, {
|