@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/dist/{EngineRuntimeRegistration.types-D1fyApMI.d.ts → EngineRuntimeRegistration.types-BZ_1XWAJ.d.ts} +2 -2
  3. package/dist/{EngineRuntimeRegistration.types-pB3FnzqR.d.cts → EngineRuntimeRegistration.types-MPYWsEM0.d.cts} +7 -2
  4. package/dist/{InMemoryRunDataFactory-Xw7v4-sj.d.cts → InMemoryRunDataFactory-hmkh0lzR.d.cts} +8 -3
  5. package/dist/{RunIntentService-BE9CAkbf.d.ts → RunIntentService-BrEq6Jm6.d.ts} +1802 -1605
  6. package/dist/{RunIntentService-siBSjaaY.d.cts → RunIntentService-MUHJ1bhO.d.cts} +1722 -1598
  7. package/dist/bootstrap/index.cjs +2 -2
  8. package/dist/bootstrap/index.d.cts +6 -3
  9. package/dist/bootstrap/index.d.ts +4 -3
  10. package/dist/bootstrap/index.js +2 -2
  11. package/dist/{bootstrap-D3r505ko.js → bootstrap-Dgzsjoj7.js} +7 -2
  12. package/dist/bootstrap-Dgzsjoj7.js.map +1 -0
  13. package/dist/{bootstrap-Cm5ruQxx.cjs → bootstrap-dVmpU1ju.cjs} +7 -2
  14. package/dist/bootstrap-dVmpU1ju.cjs.map +1 -0
  15. package/dist/{index-DeLl1Tne.d.ts → index-Bes88mxT.d.ts} +113 -6
  16. package/dist/index.cjs +71 -3
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +173 -6
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +69 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/{runtime-BGNbRnqs.js → runtime-Duf3ClPw.js} +202 -53
  23. package/dist/runtime-Duf3ClPw.js.map +1 -0
  24. package/dist/{runtime-DKXJwTNv.cjs → runtime-vH0EeZzH.cjs} +208 -53
  25. package/dist/runtime-vH0EeZzH.cjs.map +1 -0
  26. package/dist/testing.cjs +6 -2
  27. package/dist/testing.cjs.map +1 -1
  28. package/dist/testing.d.cts +3 -3
  29. package/dist/testing.d.ts +2 -2
  30. package/dist/testing.js +6 -2
  31. package/dist/testing.js.map +1 -1
  32. package/package.json +4 -13
  33. package/src/ai/AgentConnectionNodeCollector.ts +47 -5
  34. package/src/authoring/defineNode.types.ts +21 -1
  35. package/src/authoring/definePollingTrigger.types.ts +20 -0
  36. package/src/binaries/UnavailableBinaryStorage.ts +6 -0
  37. package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +9 -0
  38. package/src/browser.ts +1 -0
  39. package/src/contracts/AgentBindError.ts +11 -0
  40. package/src/contracts/CodemationTelemetryAttributeNames.ts +4 -0
  41. package/src/contracts/NoOpAgentMcpIntegration.ts +13 -0
  42. package/src/contracts/agentMcpTypes.ts +64 -0
  43. package/src/contracts/executionPersistenceContracts.ts +5 -0
  44. package/src/contracts/index.ts +4 -0
  45. package/src/contracts/mcpTypes.ts +29 -0
  46. package/src/contracts/runTypes.ts +13 -0
  47. package/src/contracts/runtimeTypes.ts +10 -0
  48. package/src/contracts/workflowTypes.ts +21 -0
  49. package/src/contracts.ts +3 -0
  50. package/src/credentials/OAuthFlowExecutor.types.ts +45 -0
  51. package/src/di/CoreTokens.ts +7 -0
  52. package/src/execution/InProcessRetryRunner.ts +31 -5
  53. package/src/execution/NodeExecutionSnapshotFactory.ts +3 -0
  54. package/src/execution/NodeExecutor.ts +27 -7
  55. package/src/execution/NodeRunStateWriter.ts +14 -0
  56. package/src/index.ts +10 -0
  57. package/src/orchestration/RunContinuationService.ts +6 -2
  58. package/src/runStorage/InMemoryBinaryStorageRegistry.ts +10 -0
  59. package/src/scheduler/InlineDrivingScheduler.ts +26 -22
  60. package/src/testing/SubWorkflowRunnerTestNode.ts +1 -0
  61. package/src/types/index.ts +1 -0
  62. package/src/validation/WorkflowEdgePortError.types.ts +16 -0
  63. package/src/validation/WorkflowEdgePortValidator.ts +52 -0
  64. package/src/workflow/definition/ConnectionInvocationIdFactory.ts +4 -3
  65. package/src/workflow/definition/ConnectionNodeIdFactory.ts +25 -0
  66. package/src/workflow/definition/NodeIterationIdFactory.ts +5 -3
  67. package/src/workflowSnapshots/WorkflowSnapshotCodec.ts +42 -10
  68. package/dist/bootstrap-Cm5ruQxx.cjs.map +0 -1
  69. package/dist/bootstrap-D3r505ko.js.map +0 -1
  70. package/dist/runtime-BGNbRnqs.js.map +0 -1
  71. 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
 
@@ -16,4 +16,10 @@ export class UnavailableBinaryStorage implements BinaryStorage {
16
16
  }
17
17
 
18
18
  async delete(): Promise<void> {}
19
+
20
+ async deleteMany(): Promise<void> {}
21
+
22
+ async listByPrefix(): Promise<ReadonlyArray<string>> {
23
+ return [];
24
+ }
19
25
  }
@@ -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 {
@@ -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
+ }
@@ -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>(policy: RetryPolicySpec | undefined, work: () => Promise<T>): Promise<T> {
26
- const spec = InProcessRetryRunner.normalizePolicy(policy);
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(policy: RetryPolicySpec | undefined): NormalizedPolicy {
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 maxAttempts = InProcessRetryRunner.assertPositiveInt(p.maxAttempts, "fixed.maxAttempts");
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.assertPositiveInt(p.maxAttempts, "exponential.maxAttempts"),
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(policy, async () => {
43
- const nodeInstance = this.nodeInstanceFactory.createByType(request.ctx.config.type);
44
- if (request.kind === "multi") {
45
- return await this.executeMultiInputActivation(request, nodeInstance);
46
- }
47
- return await this.executeSingleInputNode(request, nodeInstance);
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 = state.executionOptions?.maxNodeActivations ?? Number.MAX_SAFE_INTEGER;
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 = args.state.executionOptions?.maxNodeActivations ?? Number.MAX_SAFE_INTEGER;
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({