@codemation/core 0.10.1 → 0.11.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 +195 -0
- package/dist/{EngineRuntimeRegistration.types-D1fyApMI.d.ts → EngineRuntimeRegistration.types-BZ_1XWAJ.d.ts} +2 -2
- package/dist/{EngineRuntimeRegistration.types-pB3FnzqR.d.cts → EngineRuntimeRegistration.types-MPYWsEM0.d.cts} +7 -2
- package/dist/{InMemoryRunDataFactory-Xw7v4-sj.d.cts → InMemoryRunDataFactory-hmkh0lzR.d.cts} +8 -3
- package/dist/{RunIntentService-BE9CAkbf.d.ts → RunIntentService-BrEq6Jm6.d.ts} +1802 -1605
- package/dist/{RunIntentService-siBSjaaY.d.cts → RunIntentService-MUHJ1bhO.d.cts} +1722 -1598
- package/dist/bootstrap/index.cjs +2 -2
- package/dist/bootstrap/index.d.cts +6 -3
- package/dist/bootstrap/index.d.ts +4 -3
- package/dist/bootstrap/index.js +2 -2
- package/dist/{bootstrap-D3r505ko.js → bootstrap-Dgzsjoj7.js} +7 -2
- package/dist/bootstrap-Dgzsjoj7.js.map +1 -0
- package/dist/{bootstrap-Cm5ruQxx.cjs → bootstrap-dVmpU1ju.cjs} +7 -2
- package/dist/bootstrap-dVmpU1ju.cjs.map +1 -0
- package/dist/{index-DeLl1Tne.d.ts → index-Bes88mxT.d.ts} +113 -6
- package/dist/index.cjs +71 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +173 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +69 -4
- package/dist/index.js.map +1 -1
- package/dist/{runtime-BGNbRnqs.js → runtime-Duf3ClPw.js} +202 -53
- package/dist/runtime-Duf3ClPw.js.map +1 -0
- package/dist/{runtime-DKXJwTNv.cjs → runtime-vH0EeZzH.cjs} +208 -53
- package/dist/runtime-vH0EeZzH.cjs.map +1 -0
- package/dist/testing.cjs +6 -2
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -3
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +6 -2
- package/dist/testing.js.map +1 -1
- package/package.json +4 -13
- package/src/ai/AgentConnectionNodeCollector.ts +47 -5
- package/src/authoring/defineNode.types.ts +21 -1
- package/src/authoring/definePollingTrigger.types.ts +20 -0
- package/src/binaries/UnavailableBinaryStorage.ts +6 -0
- package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +9 -0
- package/src/browser.ts +1 -0
- package/src/contracts/AgentBindError.ts +11 -0
- package/src/contracts/CodemationTelemetryAttributeNames.ts +4 -0
- package/src/contracts/NoOpAgentMcpIntegration.ts +13 -0
- package/src/contracts/agentMcpTypes.ts +64 -0
- package/src/contracts/executionPersistenceContracts.ts +5 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/mcpTypes.ts +29 -0
- package/src/contracts/runTypes.ts +13 -0
- package/src/contracts/runtimeTypes.ts +10 -0
- package/src/contracts/workflowTypes.ts +21 -0
- package/src/contracts.ts +3 -0
- package/src/credentials/OAuthFlowExecutor.types.ts +45 -0
- package/src/di/CoreTokens.ts +7 -0
- package/src/execution/InProcessRetryRunner.ts +31 -5
- package/src/execution/NodeExecutionSnapshotFactory.ts +3 -0
- package/src/execution/NodeExecutor.ts +27 -7
- package/src/execution/NodeRunStateWriter.ts +14 -0
- package/src/index.ts +10 -0
- package/src/orchestration/RunContinuationService.ts +6 -2
- package/src/runStorage/InMemoryBinaryStorageRegistry.ts +10 -0
- package/src/scheduler/InlineDrivingScheduler.ts +26 -22
- package/src/testing/SubWorkflowRunnerTestNode.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/validation/WorkflowEdgePortError.types.ts +16 -0
- package/src/validation/WorkflowEdgePortValidator.ts +52 -0
- package/src/workflow/definition/ConnectionInvocationIdFactory.ts +4 -3
- package/src/workflow/definition/ConnectionNodeIdFactory.ts +25 -0
- package/src/workflow/definition/NodeIterationIdFactory.ts +5 -3
- package/src/workflowSnapshots/WorkflowSnapshotCodec.ts +42 -10
- package/dist/bootstrap-Cm5ruQxx.cjs.map +0 -1
- package/dist/bootstrap-D3r505ko.js.map +0 -1
- package/dist/runtime-BGNbRnqs.js.map +0 -1
- package/dist/runtime-DKXJwTNv.cjs.map +0 -1
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
Items,
|
|
12
12
|
JsonValue,
|
|
13
13
|
NodeExecutionContext,
|
|
14
|
+
NodeInspectorSummaryRow,
|
|
14
15
|
NodeOutputs,
|
|
15
16
|
TestableTriggerNode,
|
|
16
17
|
TriggerNodeConfig,
|
|
@@ -99,6 +100,15 @@ export interface DefinePollingTriggerOptions<
|
|
|
99
100
|
readonly configSchema?: ZodType<TConfig>;
|
|
100
101
|
/** Credential bindings keyed by slot (same format as `defineNode`). */
|
|
101
102
|
readonly credentials?: TBindings;
|
|
103
|
+
/**
|
|
104
|
+
* Static configuration summary surfaced in the workflow inspector — see
|
|
105
|
+
* {@link import("../contracts/workflowTypes").NodeConfigBase.inspectorSummary}.
|
|
106
|
+
*
|
|
107
|
+
* Receives the static config; returns 2–6 short label/value pairs (or `undefined` to skip).
|
|
108
|
+
*/
|
|
109
|
+
readonly inspectorSummary?: (
|
|
110
|
+
args: Readonly<{ config: TConfig }>,
|
|
111
|
+
) => ReadonlyArray<NodeInspectorSummaryRow> | undefined;
|
|
102
112
|
/**
|
|
103
113
|
* Called once when the trigger arms (or re-arms after a server restart) to provide the
|
|
104
114
|
* initial value for `state` when no persisted state exists.
|
|
@@ -201,6 +211,9 @@ export class DefinedPollingTriggerConfig<TConfig extends CredentialJsonRecord, T
|
|
|
201
211
|
icon: string | undefined,
|
|
202
212
|
private readonly credentialRequirements: ReadonlyArray<CredentialRequirement>,
|
|
203
213
|
public readonly id?: string,
|
|
214
|
+
private readonly inspectorSummaryFn?: (
|
|
215
|
+
args: Readonly<{ config: TConfig }>,
|
|
216
|
+
) => ReadonlyArray<NodeInspectorSummaryRow> | undefined,
|
|
204
217
|
) {
|
|
205
218
|
this.type = typeToken;
|
|
206
219
|
this.icon = icon;
|
|
@@ -209,6 +222,10 @@ export class DefinedPollingTriggerConfig<TConfig extends CredentialJsonRecord, T
|
|
|
209
222
|
getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
|
|
210
223
|
return this.credentialRequirements;
|
|
211
224
|
}
|
|
225
|
+
|
|
226
|
+
inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> | undefined {
|
|
227
|
+
return this.inspectorSummaryFn?.({ config: this.cfg });
|
|
228
|
+
}
|
|
212
229
|
}
|
|
213
230
|
|
|
214
231
|
// ---------------------------------------------------------------------------
|
|
@@ -376,6 +393,9 @@ export function definePollingTrigger<
|
|
|
376
393
|
options.icon,
|
|
377
394
|
credentialRequirements,
|
|
378
395
|
id,
|
|
396
|
+
options.inspectorSummary as
|
|
397
|
+
| ((args: Readonly<{ config: TConfig }>) => ReadonlyArray<NodeInspectorSummaryRow> | undefined)
|
|
398
|
+
| undefined,
|
|
379
399
|
);
|
|
380
400
|
},
|
|
381
401
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { instanceCachingFactory, type DependencyContainer } from "../../di";
|
|
2
2
|
import { CoreTokens } from "../../di";
|
|
3
|
+
import { NoOpAgentMcpIntegration } from "../../contracts/NoOpAgentMcpIntegration";
|
|
3
4
|
import { EngineExecutionLimitsPolicyFactory } from "../../policies/executionLimits/EngineExecutionLimitsPolicyFactory";
|
|
4
5
|
import {
|
|
5
6
|
ChildExecutionScopeFactory,
|
|
@@ -39,6 +40,14 @@ export class EngineRuntimeRegistrar {
|
|
|
39
40
|
this.registerDefaultActivationScheduler(container);
|
|
40
41
|
this.registerEngine(container, options);
|
|
41
42
|
this.registerIntentServices(container);
|
|
43
|
+
this.registerAgentMcpIntegration(container);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private registerAgentMcpIntegration(container: DependencyContainer): void {
|
|
47
|
+
if (container.isRegistered(CoreTokens.AgentMcpIntegration, true)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
container.registerInstance(CoreTokens.AgentMcpIntegration, new NoOpAgentMcpIntegration());
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
private registerSupportFactories(container: DependencyContainer): void {
|
package/src/browser.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type {
|
|
|
6
6
|
AgentConnectionCredentialSource,
|
|
7
7
|
AgentConnectionNodeDescriptor,
|
|
8
8
|
AgentConnectionNodeRole,
|
|
9
|
+
McpServerResolver,
|
|
9
10
|
} from "./ai/AgentConnectionNodeCollector";
|
|
10
11
|
export type { AgentNodeConfig } from "./ai/AiHost";
|
|
11
12
|
export { ConnectionNodeIdFactory } from "./workflow/definition/ConnectionNodeIdFactory";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown at agent bind time when mcpServers declarations cannot be resolved.
|
|
3
|
+
* Causes include: unknown server id, missing credential instance, insufficient scopes,
|
|
4
|
+
* and ambiguous shorthand binding (multiple credential instances match).
|
|
5
|
+
*/
|
|
6
|
+
export class AgentBindError extends Error {
|
|
7
|
+
constructor(message: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "AgentBindError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -15,4 +15,8 @@ export class CodemationTelemetryAttributeNames {
|
|
|
15
15
|
static readonly iterationIndex = "codemation.iteration.index";
|
|
16
16
|
/** Set when this span/metric was recorded under a sub-agent triggered by an outer LLM/tool call. */
|
|
17
17
|
static readonly parentInvocationId = "codemation.parent.invocation_id";
|
|
18
|
+
/** MCP server id on spans created for callTool invocations. */
|
|
19
|
+
static readonly mcpServerId = "mcp.server_id";
|
|
20
|
+
/** MCP tool name on spans created for callTool invocations. */
|
|
21
|
+
static readonly mcpToolName = "mcp.tool_name";
|
|
18
22
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentMcpIntegration, AgentMcpToolMap } from "./agentMcpTypes";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* No-op implementation of AgentMcpIntegration.
|
|
5
|
+
* Registered by the core engine runtime as a fallback when the host does not
|
|
6
|
+
* supply a real implementation (e.g. in unit tests or headless engine setups).
|
|
7
|
+
* Always returns an empty tool map so the agent runs with node-backed tools only.
|
|
8
|
+
*/
|
|
9
|
+
export class NoOpAgentMcpIntegration implements AgentMcpIntegration {
|
|
10
|
+
async prepareMcpTools(): Promise<AgentMcpToolMap> {
|
|
11
|
+
return new Map();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { NodeId, WorkflowId } from "./baseTypes";
|
|
2
|
+
import type { ConnectionInvocationAppendArgs, ConnectionInvocationId } from "./runTypes";
|
|
3
|
+
import type { TelemetrySpanEventRecord } from "./telemetryTypes";
|
|
4
|
+
import type { NodeActivationId, NodeIterationId } from "./workflowTypes";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Emitted as a span event when a credential is missing required scopes
|
|
8
|
+
* (bind-time) or when callTool returns a permission error (runtime).
|
|
9
|
+
* The credential type id can be looked up from the credential instance when needed.
|
|
10
|
+
*/
|
|
11
|
+
export interface NeedsReconsentEvent {
|
|
12
|
+
readonly serverId: string;
|
|
13
|
+
readonly credentialInstanceId: string;
|
|
14
|
+
readonly missingScopesHint?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An opaque MCP tool map: keyed by serverId → (toolName → tool definition).
|
|
19
|
+
* Typed as unknown so core does not depend on the AI SDK's ToolSet type.
|
|
20
|
+
* AIAgentNode (in core-nodes, which does depend on ai) casts this to
|
|
21
|
+
* ReadonlyMap<string, ToolSet> before passing to DeferredMetaToolStrategyFactory.
|
|
22
|
+
*/
|
|
23
|
+
export type AgentMcpToolMap = ReadonlyMap<string, Readonly<Record<string, unknown>>>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Contract implemented by the host. Resolves MCP server bindings for an agent run
|
|
27
|
+
* via the standard credential-binding table (one slot per declared server, keyed
|
|
28
|
+
* by `(workflowId, mcpConnectionNodeId, "credential")`), and returns a ready-to-use
|
|
29
|
+
* tool map with wrapped execute callbacks for telemetry and 403 detection.
|
|
30
|
+
* Core-nodes imports this interface so AIAgentNode can inject it without
|
|
31
|
+
* depending on the host.
|
|
32
|
+
*/
|
|
33
|
+
export interface AgentMcpIntegration {
|
|
34
|
+
/**
|
|
35
|
+
* Look up the credential binding per server, validate scopes, open pool
|
|
36
|
+
* connections, and return a tool map keyed by serverId. Each tool's
|
|
37
|
+
* execute callback includes:
|
|
38
|
+
* - Telemetry child span (mcp.server_id, mcp.tool_name attributes)
|
|
39
|
+
* - 403/permission error detection → emits a NeedsReconsentEvent span event
|
|
40
|
+
*
|
|
41
|
+
* Throws `AgentBindError` on validation failures (missing server, unbound
|
|
42
|
+
* credential slot, missing credential instance, insufficient scopes).
|
|
43
|
+
*/
|
|
44
|
+
prepareMcpTools(args: {
|
|
45
|
+
readonly workflowId: WorkflowId;
|
|
46
|
+
readonly agentNodeId: NodeId;
|
|
47
|
+
readonly serverIds: ReadonlyArray<string>;
|
|
48
|
+
readonly pinnedMcpTools: readonly string[];
|
|
49
|
+
readonly emitSpanEvent: (event: TelemetrySpanEventRecord) => void;
|
|
50
|
+
readonly startChildSpan: (args: { readonly name: string; readonly attributes?: Record<string, string> }) => {
|
|
51
|
+
readonly end: (args?: { status?: "ok" | "error"; statusMessage?: string }) => void;
|
|
52
|
+
};
|
|
53
|
+
/** Per-MCP-tool-call invocation appender. Optional; when omitted the wrapper emits only telemetry spans. */
|
|
54
|
+
readonly appendMcpInvocation?: (args: ConnectionInvocationAppendArgs) => Promise<void>;
|
|
55
|
+
/** Agent activation id to attach to each invocation record (used by canvas + inspector grouping). */
|
|
56
|
+
readonly parentAgentActivationId?: NodeActivationId;
|
|
57
|
+
/** Per-item iteration id when the agent runs inside a per-item loop. */
|
|
58
|
+
readonly iterationId?: NodeIterationId;
|
|
59
|
+
/** Item index (0-based) of the iteration that owns these tool calls. */
|
|
60
|
+
readonly itemIndex?: number;
|
|
61
|
+
/** Parent invocation id when this agent is itself executing as a sub-agent. */
|
|
62
|
+
readonly parentInvocationId?: ConnectionInvocationId;
|
|
63
|
+
}): Promise<AgentMcpToolMap>;
|
|
64
|
+
}
|
|
@@ -208,6 +208,11 @@ export interface ExecutionInstanceDto {
|
|
|
208
208
|
readonly itemIndex?: number;
|
|
209
209
|
/** Parent invocation id when this instance was emitted by a sub-agent triggered by an outer LLM/tool call. */
|
|
210
210
|
readonly parentInvocationId?: string;
|
|
211
|
+
/**
|
|
212
|
+
* When this instance is a SubWorkflow node activation, the run id of the child run it spawned.
|
|
213
|
+
* Used by the UI to deep-link directly to the child execution.
|
|
214
|
+
*/
|
|
215
|
+
readonly childRunId?: string;
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
export interface WorkflowDetailSelectionState {
|
package/src/contracts/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
export * from "./AgentBindError";
|
|
2
|
+
export * from "./agentMcpTypes";
|
|
3
|
+
export * from "./NoOpAgentMcpIntegration";
|
|
1
4
|
export * from "./baseTypes";
|
|
2
5
|
export * from "./assertionTypes";
|
|
6
|
+
export * from "./mcpTypes";
|
|
3
7
|
export * from "./collectionTypes";
|
|
4
8
|
export * from "./credentialTypes";
|
|
5
9
|
export * from "./emitPorts";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type McpServerTransport = "http";
|
|
2
|
+
// "stdio" is a self-host-only escape hatch (CODEMATION_ALLOW_STDIO_MCP=true); not typed here for managed.
|
|
3
|
+
|
|
4
|
+
export interface McpServerDeclaration {
|
|
5
|
+
/** Globally unique slug, e.g. "gmail". Workflow authors reference this. */
|
|
6
|
+
id: string;
|
|
7
|
+
displayName: string;
|
|
8
|
+
description: string;
|
|
9
|
+
transport: McpServerTransport;
|
|
10
|
+
url: string;
|
|
11
|
+
/**
|
|
12
|
+
* Credential types accepted by this MCP server, matching CredentialRequirement.acceptedTypes.
|
|
13
|
+
* Absent or empty means no credential is required.
|
|
14
|
+
*/
|
|
15
|
+
acceptedCredentialTypes?: ReadonlyArray<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Documentation only in MVP. The bind-time validator checks
|
|
18
|
+
* requiredScopes ⊆ CredentialInstance.scopesGranted.
|
|
19
|
+
*/
|
|
20
|
+
requiredScopes?: string[];
|
|
21
|
+
/** Non-secret static headers merged onto every MCP request. */
|
|
22
|
+
staticHeaders?: Record<string, string>;
|
|
23
|
+
/**
|
|
24
|
+
* Overrides for tool descriptions advertised by the MCP server.
|
|
25
|
+
* Applied by the connection pool after tools/list.
|
|
26
|
+
* Key: exact tool name as returned by the server.
|
|
27
|
+
*/
|
|
28
|
+
toolDescriptionOverrides?: Record<string, string>;
|
|
29
|
+
}
|
|
@@ -88,6 +88,8 @@ export interface PersistedWorkflowSnapshotNode {
|
|
|
88
88
|
tokenName?: string;
|
|
89
89
|
configTokenName?: string;
|
|
90
90
|
config: unknown;
|
|
91
|
+
/** Pre-computed static configuration summary; populated by WorkflowSnapshotCodec. */
|
|
92
|
+
inspectorSummary?: ReadonlyArray<Readonly<{ label: string; value: string }>>;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
export interface PersistedWorkflowSnapshot {
|
|
@@ -150,6 +152,11 @@ export interface NodeExecutionSnapshot {
|
|
|
150
152
|
inputsByPort?: NodeInputsByPort;
|
|
151
153
|
outputs?: NodeOutputs;
|
|
152
154
|
error?: NodeExecutionError;
|
|
155
|
+
/**
|
|
156
|
+
* When the node is a SubWorkflow invocation, the run id of the child run it spawned.
|
|
157
|
+
* Populated after the child run completes so the UI can deep-link to that specific execution.
|
|
158
|
+
*/
|
|
159
|
+
childRunId?: RunId;
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
/** Stable id for a single connection invocation row in {@link ConnectionInvocationRecord}. */
|
|
@@ -169,6 +176,10 @@ export interface ConnectionInvocationRecord {
|
|
|
169
176
|
readonly status: NodeExecutionStatus;
|
|
170
177
|
readonly managedInput?: JsonValue;
|
|
171
178
|
readonly managedOutput?: JsonValue;
|
|
179
|
+
/** Short human-readable description of what this invocation is doing right now (e.g. `"calling search_messages"`). Rendered as a sub-line on the canvas node card. */
|
|
180
|
+
readonly statusLabel?: string;
|
|
181
|
+
/** Stable identifier for the thing this invocation acts on (e.g. an MCP tool name like `"search_messages"`). Persists across status transitions so the inspector can show it on completed/failed entries too. Connection nodes that ARE the tool (e.g. node-backed agent tools) leave this unset — the parent node id already identifies the subject. */
|
|
182
|
+
readonly subjectName?: string;
|
|
172
183
|
readonly error?: NodeExecutionError;
|
|
173
184
|
readonly queuedAt?: string;
|
|
174
185
|
readonly startedAt?: string;
|
|
@@ -191,6 +202,8 @@ export type ConnectionInvocationAppendArgs = Readonly<{
|
|
|
191
202
|
status: NodeExecutionStatus;
|
|
192
203
|
managedInput?: JsonValue;
|
|
193
204
|
managedOutput?: JsonValue;
|
|
205
|
+
statusLabel?: string;
|
|
206
|
+
subjectName?: string;
|
|
194
207
|
error?: NodeExecutionError;
|
|
195
208
|
queuedAt?: string;
|
|
196
209
|
startedAt?: string;
|
|
@@ -91,6 +91,12 @@ export interface NodeExecutionStatePublisher {
|
|
|
91
91
|
error: Error;
|
|
92
92
|
}): Promise<void>;
|
|
93
93
|
appendConnectionInvocation(args: ConnectionInvocationAppendArgs): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Annotates the current snapshot for `nodeId` with the id of the child run spawned by a
|
|
96
|
+
* SubWorkflow invocation. Called from `SubWorkflowNode.execute` after `runById` resolves.
|
|
97
|
+
* The engine's subsequent `markCompleted` call preserves the value via `previous.childRunId`.
|
|
98
|
+
*/
|
|
99
|
+
setChildRunId?(args: { nodeId: NodeId; childRunId: RunId }): Promise<void>;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
export type BinaryBody = BinaryReadableStream<Uint8Array> | AsyncIterable<Uint8Array> | Uint8Array | ArrayBuffer;
|
|
@@ -122,6 +128,10 @@ export interface BinaryStorage {
|
|
|
122
128
|
openReadStream(storageKey: string): Promise<BinaryStorageReadResult | undefined>;
|
|
123
129
|
stat(storageKey: string): Promise<BinaryStorageStatResult>;
|
|
124
130
|
delete(storageKey: string): Promise<void>;
|
|
131
|
+
/** Deletes multiple objects in bulk. Keys are batched internally. */
|
|
132
|
+
deleteMany(storageKeys: ReadonlyArray<string>): Promise<void>;
|
|
133
|
+
/** Lists all keys sharing a common prefix. Returns keys in arbitrary order. */
|
|
134
|
+
listByPrefix(prefix: string): Promise<ReadonlyArray<string>>;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
export interface BinaryAttachmentCreateRequest {
|
|
@@ -102,6 +102,27 @@ export interface NodeConfigBase {
|
|
|
102
102
|
* configs (e.g. `AssertionNodeConfig`, `StringEqualsAssertionNodeConfig`).
|
|
103
103
|
*/
|
|
104
104
|
readonly emitsAssertions?: true;
|
|
105
|
+
/**
|
|
106
|
+
* Static configuration summary surfaced in the workflow inspector — the design-time
|
|
107
|
+
* "what does this node do" panel that renders before any run telemetry exists.
|
|
108
|
+
*
|
|
109
|
+
* Return 2–6 short label/value pairs derived from this config (method + url for an HTTP
|
|
110
|
+
* call, model + tool list for an agent, schedule + timezone for a cron trigger, etc.).
|
|
111
|
+
* Values are truncated by the UI; aim for one line each. Return `undefined` to opt out
|
|
112
|
+
* — the inspector hides the section when no rows are produced.
|
|
113
|
+
*
|
|
114
|
+
* Implement on the config class instance so the function can read sibling config fields.
|
|
115
|
+
* `defineNode({ inspectorSummary })` plumbs through to this.
|
|
116
|
+
*/
|
|
117
|
+
inspectorSummary?(): ReadonlyArray<NodeInspectorSummaryRow> | undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* One row of a node's static configuration summary. See {@link NodeConfigBase.inspectorSummary}.
|
|
122
|
+
*/
|
|
123
|
+
export interface NodeInspectorSummaryRow {
|
|
124
|
+
readonly label: string;
|
|
125
|
+
readonly value: string;
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
export declare const runnableNodeInputType: unique symbol;
|
package/src/contracts.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Pure-type-only re-exports. Use this for type-only consumers that should not drag in runtime DSL or factory code.
|
|
2
2
|
// This subpath prevents unnecessary compile-graph bloat for packages that only need types like NodeId, Items, etc.
|
|
3
3
|
|
|
4
|
+
export type * from "./contracts/agentMcpTypes";
|
|
5
|
+
export * from "./contracts/AgentBindError";
|
|
6
|
+
export * from "./contracts/NoOpAgentMcpIntegration";
|
|
4
7
|
export type * from "./contracts/baseTypes";
|
|
5
8
|
export type * from "./contracts/assertionTypes";
|
|
6
9
|
// assertionTypes also exports a runtime helper for deriving pass/fail from a score+threshold.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Four-concept model for credentials (see docs/design/credentials-oauth-unification.md):
|
|
3
|
+
* 1. CredentialType — schema for stored material (e.g. "oauth.google.gmail").
|
|
4
|
+
* 2. Credential slot requirement — which types a node or MCP server accepts.
|
|
5
|
+
* 3. CredentialInstance — a stored, usable token row in the host's credential store.
|
|
6
|
+
* 4. OAuthFlowExecutor (this file) — the only concept that differs between deployment
|
|
7
|
+
* modes. DI selects one implementation at boot; the rest of the system programs
|
|
8
|
+
* against this interface alone.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface OAuthFlowStartArgs {
|
|
12
|
+
readonly typeId: string;
|
|
13
|
+
readonly scopes: ReadonlyArray<string>;
|
|
14
|
+
readonly redirectUri: string;
|
|
15
|
+
readonly instanceId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OAuthFlowStartResult {
|
|
19
|
+
readonly consentUrl: string;
|
|
20
|
+
readonly stateToken: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OAuthFlowCallbackArgs {
|
|
24
|
+
readonly stateToken: string;
|
|
25
|
+
readonly code: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface OAuthMaterial {
|
|
29
|
+
readonly accessToken: string;
|
|
30
|
+
readonly refreshToken?: string;
|
|
31
|
+
readonly expiresAt?: string;
|
|
32
|
+
readonly grantedScopes: ReadonlyArray<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OAuthFlowExecutor {
|
|
36
|
+
start(args: OAuthFlowStartArgs): Promise<OAuthFlowStartResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Returns the instanceId associated with a pending stateToken without consuming it.
|
|
39
|
+
* Used by callback routes to identify the target instance before calling completeCallback.
|
|
40
|
+
* Returns undefined when the stateToken is unknown or already consumed.
|
|
41
|
+
*/
|
|
42
|
+
lookupInstanceId(stateToken: string): string | undefined;
|
|
43
|
+
completeCallback(args: OAuthFlowCallbackArgs): Promise<OAuthMaterial>;
|
|
44
|
+
refresh(args: { typeId: string; instanceId: string; material: OAuthMaterial }): Promise<OAuthMaterial>;
|
|
45
|
+
}
|
package/src/di/CoreTokens.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TypeToken } from "./index";
|
|
2
2
|
import type { RunEventBus } from "../events/runEvents";
|
|
3
3
|
import type { EngineExecutionLimitsPolicy } from "../policies/executionLimits/EngineExecutionLimitsPolicy";
|
|
4
|
+
import type { AgentMcpIntegration } from "../contracts/agentMcpTypes";
|
|
4
5
|
import type {
|
|
5
6
|
ActivationIdFactory,
|
|
6
7
|
BinaryStorage,
|
|
@@ -57,4 +58,10 @@ export const CoreTokens = {
|
|
|
57
58
|
WorkflowActivationPolicy: Symbol.for(
|
|
58
59
|
"codemation.core.WorkflowActivationPolicy",
|
|
59
60
|
) as TypeToken<WorkflowActivationPolicy>,
|
|
61
|
+
/**
|
|
62
|
+
* Optional. When registered, AIAgentNode uses it to resolve mcpServers bindings,
|
|
63
|
+
* validate scopes, open pool connections, and prepare the MCP ToolSet map.
|
|
64
|
+
* Not registered in the default core bootstrap — the host provides the implementation.
|
|
65
|
+
*/
|
|
66
|
+
AgentMcpIntegration: Symbol.for("codemation.core.AgentMcpIntegration") as TypeToken<AgentMcpIntegration>,
|
|
60
67
|
} as const;
|
|
@@ -7,6 +7,9 @@ import type { AsyncSleeper } from "./asyncSleeper.types";
|
|
|
7
7
|
|
|
8
8
|
export type { AsyncSleeper } from "./asyncSleeper.types";
|
|
9
9
|
|
|
10
|
+
/** Maximum permitted retry attempts — workflow-declared values above this are clamped. */
|
|
11
|
+
const HARD_MAX_RETRY_ATTEMPTS = 10;
|
|
12
|
+
|
|
10
13
|
type NormalizedPolicy =
|
|
11
14
|
| { readonly kind: "none"; readonly maxAttempts: 1 }
|
|
12
15
|
| { readonly kind: "fixed"; readonly maxAttempts: number; readonly delayMs: number }
|
|
@@ -22,8 +25,13 @@ type NormalizedPolicy =
|
|
|
22
25
|
export class InProcessRetryRunner {
|
|
23
26
|
constructor(private readonly sleeper: AsyncSleeper) {}
|
|
24
27
|
|
|
25
|
-
async run<T>(
|
|
26
|
-
|
|
28
|
+
async run<T>(
|
|
29
|
+
policy: RetryPolicySpec | undefined,
|
|
30
|
+
work: () => Promise<T>,
|
|
31
|
+
shouldRetry?: (error: unknown) => boolean,
|
|
32
|
+
warn?: (message: string) => void,
|
|
33
|
+
): Promise<T> {
|
|
34
|
+
const spec = InProcessRetryRunner.normalizePolicy(policy, warn);
|
|
27
35
|
let lastError: unknown;
|
|
28
36
|
for (let attempt = 1; attempt <= spec.maxAttempts; attempt++) {
|
|
29
37
|
try {
|
|
@@ -33,6 +41,9 @@ export class InProcessRetryRunner {
|
|
|
33
41
|
if (attempt >= spec.maxAttempts) {
|
|
34
42
|
break;
|
|
35
43
|
}
|
|
44
|
+
if (shouldRetry !== undefined && !shouldRetry(error)) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
36
47
|
const delayMs = InProcessRetryRunner.delayAfterFailureMs(spec, attempt);
|
|
37
48
|
await this.sleeper.sleep(delayMs);
|
|
38
49
|
}
|
|
@@ -58,7 +69,10 @@ export class InProcessRetryRunner {
|
|
|
58
69
|
return Math.max(0, Math.floor(ms));
|
|
59
70
|
}
|
|
60
71
|
|
|
61
|
-
private static normalizePolicy(
|
|
72
|
+
private static normalizePolicy(
|
|
73
|
+
policy: RetryPolicySpec | undefined,
|
|
74
|
+
warn?: (message: string) => void,
|
|
75
|
+
): NormalizedPolicy {
|
|
62
76
|
if (policy === undefined) {
|
|
63
77
|
return { kind: "none", maxAttempts: 1 };
|
|
64
78
|
}
|
|
@@ -71,15 +85,17 @@ export class InProcessRetryRunner {
|
|
|
71
85
|
}
|
|
72
86
|
if (kind === "fixed") {
|
|
73
87
|
const p = policy as FixedRetryPolicySpec;
|
|
74
|
-
const
|
|
88
|
+
const raw = InProcessRetryRunner.assertPositiveInt(p.maxAttempts, "fixed.maxAttempts");
|
|
89
|
+
const maxAttempts = InProcessRetryRunner.clampMaxAttempts(raw, warn);
|
|
75
90
|
const delayMs = InProcessRetryRunner.assertNonNegativeFinite(p.delayMs, "fixed.delayMs");
|
|
76
91
|
return { kind: "fixed", maxAttempts, delayMs };
|
|
77
92
|
}
|
|
78
93
|
if (kind === "exponential") {
|
|
79
94
|
const p = policy as ExponentialRetryPolicySpec;
|
|
95
|
+
const raw = InProcessRetryRunner.assertPositiveInt(p.maxAttempts, "exponential.maxAttempts");
|
|
80
96
|
return {
|
|
81
97
|
kind: "exponential",
|
|
82
|
-
maxAttempts: InProcessRetryRunner.
|
|
98
|
+
maxAttempts: InProcessRetryRunner.clampMaxAttempts(raw, warn),
|
|
83
99
|
initialDelayMs: InProcessRetryRunner.assertNonNegativeFinite(p.initialDelayMs, "exponential.initialDelayMs"),
|
|
84
100
|
multiplier: InProcessRetryRunner.assertMultiplier(p.multiplier),
|
|
85
101
|
maxDelayMs:
|
|
@@ -92,6 +108,16 @@ export class InProcessRetryRunner {
|
|
|
92
108
|
return { kind: "none", maxAttempts: 1 };
|
|
93
109
|
}
|
|
94
110
|
|
|
111
|
+
private static clampMaxAttempts(requested: number, warn?: (message: string) => void): number {
|
|
112
|
+
if (requested > HARD_MAX_RETRY_ATTEMPTS) {
|
|
113
|
+
warn?.(
|
|
114
|
+
`Retry policy maxAttempts (${requested}) exceeds hard ceiling (${HARD_MAX_RETRY_ATTEMPTS}); clamping to ${HARD_MAX_RETRY_ATTEMPTS}.`,
|
|
115
|
+
);
|
|
116
|
+
return HARD_MAX_RETRY_ATTEMPTS;
|
|
117
|
+
}
|
|
118
|
+
return requested;
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
private static assertPositiveInt(value: unknown, label: string): number {
|
|
96
122
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 1 || !Number.isInteger(value)) {
|
|
97
123
|
throw new Error(`Retry policy ${label} must be a positive integer`);
|
|
@@ -88,6 +88,7 @@ export class NodeExecutionSnapshotFactory {
|
|
|
88
88
|
outputs: args.outputs,
|
|
89
89
|
usedPinnedOutput: fromPinnedOutput,
|
|
90
90
|
error: undefined,
|
|
91
|
+
...(args.previous?.childRunId !== undefined ? { childRunId: args.previous.childRunId } : {}),
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -116,6 +117,7 @@ export class NodeExecutionSnapshotFactory {
|
|
|
116
117
|
inputsByPort: args.inputsByPort,
|
|
117
118
|
outputs: args.outputs,
|
|
118
119
|
error: undefined,
|
|
120
|
+
...(args.previous?.childRunId !== undefined ? { childRunId: args.previous.childRunId } : {}),
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -149,6 +151,7 @@ export class NodeExecutionSnapshotFactory {
|
|
|
149
151
|
stack: args.error.stack,
|
|
150
152
|
details: (args.error as Error & { details?: JsonValue }).details,
|
|
151
153
|
},
|
|
154
|
+
...(args.previous?.childRunId !== undefined ? { childRunId: args.previous.childRunId } : {}),
|
|
152
155
|
};
|
|
153
156
|
}
|
|
154
157
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { isPortsEmission, isUnbrandedPortsEmissionShape } from "../contracts/emitPorts";
|
|
3
|
+
import { CredentialUnboundError } from "../contracts/credentialTypes";
|
|
3
4
|
|
|
4
5
|
import type {
|
|
5
6
|
Item,
|
|
@@ -38,14 +39,33 @@ export class NodeExecutor {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
async execute(request: NodeActivationRequest): Promise<NodeOutputs> {
|
|
42
|
+
await this.assertRequiredCredentialsBound(request);
|
|
41
43
|
const policy = request.ctx.config.retryPolicy;
|
|
42
|
-
return await this.retryRunner.run(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
return await this.retryRunner.run(
|
|
45
|
+
policy,
|
|
46
|
+
async () => {
|
|
47
|
+
const nodeInstance = this.nodeInstanceFactory.createByType(request.ctx.config.type);
|
|
48
|
+
if (request.kind === "multi") {
|
|
49
|
+
return await this.executeMultiInputActivation(request, nodeInstance);
|
|
50
|
+
}
|
|
51
|
+
return await this.executeSingleInputNode(request, nodeInstance);
|
|
52
|
+
},
|
|
53
|
+
(error) => !this.isCredentialError(error),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async assertRequiredCredentialsBound(request: NodeActivationRequest): Promise<void> {
|
|
58
|
+
if (!request.ctx.getCredential) return;
|
|
59
|
+
for (const req of request.ctx.config.getCredentialRequirements?.() ?? []) {
|
|
60
|
+
if (req.optional) continue;
|
|
61
|
+
await request.ctx.getCredential(req.slotKey);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private isCredentialError(e: unknown): boolean {
|
|
66
|
+
if (e instanceof CredentialUnboundError) return true;
|
|
67
|
+
const cause = e instanceof Error ? (e as { cause?: unknown }).cause : undefined;
|
|
68
|
+
return cause instanceof CredentialUnboundError;
|
|
49
69
|
}
|
|
50
70
|
|
|
51
71
|
private async executeMultiInputActivation(
|
|
@@ -130,6 +130,18 @@ export class NodeRunStateWriter implements NodeExecutionStatePublisher {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
setChildRunId(args: { nodeId: NodeId; childRunId: RunId }): Promise<void> {
|
|
134
|
+
return this.enqueue(async () => {
|
|
135
|
+
const state = await this.loadState();
|
|
136
|
+
const previous = state.nodeSnapshotsByNodeId?.[args.nodeId];
|
|
137
|
+
if (!previous) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const updated: NodeExecutionSnapshot = { ...previous, childRunId: args.childRunId };
|
|
141
|
+
await this.saveSnapshot(state, updated);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
133
145
|
appendConnectionInvocation(args: ConnectionInvocationAppendArgs): Promise<void> {
|
|
134
146
|
return this.enqueue(async () => {
|
|
135
147
|
const state = await this.loadState();
|
|
@@ -144,6 +156,8 @@ export class NodeRunStateWriter implements NodeExecutionStatePublisher {
|
|
|
144
156
|
status: args.status,
|
|
145
157
|
managedInput: args.managedInput,
|
|
146
158
|
managedOutput: args.managedOutput,
|
|
159
|
+
statusLabel: args.statusLabel,
|
|
160
|
+
subjectName: args.subjectName,
|
|
147
161
|
error: args.error,
|
|
148
162
|
queuedAt: args.queuedAt,
|
|
149
163
|
startedAt: args.startedAt,
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type {
|
|
|
6
6
|
AgentConnectionCredentialSource,
|
|
7
7
|
AgentConnectionNodeDescriptor,
|
|
8
8
|
AgentConnectionNodeRole,
|
|
9
|
+
McpServerResolver,
|
|
9
10
|
} from "./ai/AgentConnectionNodeCollector";
|
|
10
11
|
export * from "./workflow";
|
|
11
12
|
export * from "./di";
|
|
@@ -34,3 +35,12 @@ export type {
|
|
|
34
35
|
PollingRunCycleResult,
|
|
35
36
|
PollingTriggerStartArgs,
|
|
36
37
|
} from "./triggers/polling";
|
|
38
|
+
export { WorkflowEdgePortValidator } from "./validation/WorkflowEdgePortValidator";
|
|
39
|
+
export type { WorkflowEdgePortError, WorkflowEdgePortValidationResult } from "./validation/WorkflowEdgePortError.types";
|
|
40
|
+
export type {
|
|
41
|
+
OAuthFlowStartArgs,
|
|
42
|
+
OAuthFlowStartResult,
|
|
43
|
+
OAuthFlowCallbackArgs,
|
|
44
|
+
OAuthMaterial,
|
|
45
|
+
OAuthFlowExecutor,
|
|
46
|
+
} from "./credentials/OAuthFlowExecutor.types";
|
|
@@ -161,7 +161,9 @@ export class RunContinuationService {
|
|
|
161
161
|
|
|
162
162
|
const completedActivations = (state.engineCounters?.completedNodeActivations ?? 0) + 1;
|
|
163
163
|
const engineCounters = { completedNodeActivations: completedActivations };
|
|
164
|
-
const maxNodeActivations =
|
|
164
|
+
const maxNodeActivations =
|
|
165
|
+
state.executionOptions?.maxNodeActivations ??
|
|
166
|
+
this.executionLimitsPolicy.createRootExecutionOptions().maxNodeActivations!;
|
|
165
167
|
|
|
166
168
|
if (this.semantics.isStopConditionSatisfied(state.control?.stopCondition, args.nodeId)) {
|
|
167
169
|
const completedState = this.persistedRunStateTerminalBuilder.mergeTerminal({
|
|
@@ -566,7 +568,9 @@ export class RunContinuationService {
|
|
|
566
568
|
|
|
567
569
|
const completedActivations = (args.state.engineCounters?.completedNodeActivations ?? 0) + 1;
|
|
568
570
|
const engineCounters = { completedNodeActivations: completedActivations };
|
|
569
|
-
const maxNodeActivations =
|
|
571
|
+
const maxNodeActivations =
|
|
572
|
+
args.state.executionOptions?.maxNodeActivations ??
|
|
573
|
+
this.executionLimitsPolicy.createRootExecutionOptions().maxNodeActivations!;
|
|
570
574
|
|
|
571
575
|
if (this.semantics.isStopConditionSatisfied(args.state.control?.stopCondition, args.args.nodeId)) {
|
|
572
576
|
const completedState = this.persistedRunStateTerminalBuilder.mergeTerminal({
|