@codemation/core 0.8.0 → 0.10.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 (103) hide show
  1. package/CHANGELOG.md +390 -0
  2. package/dist/{EngineRuntimeRegistration.types-BP6tsaNP.d.ts → EngineRuntimeRegistration.types-D1fyApMI.d.ts} +2 -2
  3. package/dist/{EngineWorkflowRunnerService-DzOCa1BW.d.cts → EngineRuntimeRegistration.types-pB3FnzqR.d.cts} +17 -17
  4. package/dist/{InMemoryRunDataFactory-1iz7_SnO.d.cts → InMemoryRunDataFactory-Xw7v4-sj.d.cts} +31 -29
  5. package/dist/InMemoryRunEventBusRegistry-VM3OWnHo.cjs +47 -0
  6. package/dist/InMemoryRunEventBusRegistry-VM3OWnHo.cjs.map +1 -0
  7. package/dist/InMemoryRunEventBusRegistry-sM4z4n_i.js +41 -0
  8. package/dist/InMemoryRunEventBusRegistry-sM4z4n_i.js.map +1 -0
  9. package/dist/{RunIntentService-BqhmdoA1.d.ts → RunIntentService-BE9CAkbf.d.ts} +966 -471
  10. package/dist/{RunIntentService-S-1lW-gS.d.cts → RunIntentService-siBSjaaY.d.cts} +859 -493
  11. package/dist/bootstrap/index.cjs +5 -2
  12. package/dist/bootstrap/index.d.cts +212 -135
  13. package/dist/bootstrap/index.d.ts +4 -4
  14. package/dist/bootstrap/index.js +3 -3
  15. package/dist/{bootstrap-BaN6hZ5I.cjs → bootstrap-Cm5ruQxx.cjs} +263 -12
  16. package/dist/bootstrap-Cm5ruQxx.cjs.map +1 -0
  17. package/dist/bootstrap-D3r505ko.js +454 -0
  18. package/dist/bootstrap-D3r505ko.js.map +1 -0
  19. package/dist/{index-CVs9rVhl.d.ts → index-DeLl1Tne.d.ts} +632 -230
  20. package/dist/index.cjs +323 -176
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +544 -91
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +299 -166
  25. package/dist/index.js.map +1 -1
  26. package/dist/{runtime-DUW6tIJ1.js → runtime-BGNbRnqs.js} +934 -75
  27. package/dist/runtime-BGNbRnqs.js.map +1 -0
  28. package/dist/{runtime-Dvo2ru5A.cjs → runtime-DKXJwTNv.cjs} +1028 -73
  29. package/dist/runtime-DKXJwTNv.cjs.map +1 -0
  30. package/dist/testing.cjs +5 -5
  31. package/dist/testing.cjs.map +1 -1
  32. package/dist/testing.d.cts +2 -2
  33. package/dist/testing.d.ts +2 -2
  34. package/dist/testing.js +4 -4
  35. package/dist/testing.js.map +1 -1
  36. package/package.json +7 -2
  37. package/src/ai/AiHost.ts +42 -14
  38. package/src/authoring/DefinedCollectionRegistry.ts +17 -0
  39. package/src/authoring/defineCollection.types.ts +181 -0
  40. package/src/authoring/definePollingTrigger.types.ts +396 -0
  41. package/src/authoring/definePollingTriggerInternals.ts +74 -0
  42. package/src/authoring/index.ts +19 -0
  43. package/src/bootstrap/index.ts +9 -0
  44. package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +21 -14
  45. package/src/browser.ts +1 -0
  46. package/src/contracts/CodemationTelemetryAttributeNames.ts +6 -0
  47. package/src/contracts/NoOpNodeExecutionTelemetry.ts +2 -11
  48. package/src/contracts/NoOpTelemetrySpanScope.ts +46 -10
  49. package/src/contracts/assertionTypes.ts +63 -0
  50. package/src/contracts/baseTypes.ts +12 -0
  51. package/src/contracts/collectionTypes.ts +44 -0
  52. package/src/contracts/credentialTypes.ts +23 -1
  53. package/src/contracts/executionPersistenceContracts.ts +30 -0
  54. package/src/contracts/index.ts +4 -0
  55. package/src/contracts/runTypes.ts +37 -1
  56. package/src/contracts/runtimeTypes.ts +42 -0
  57. package/src/contracts/telemetryTypes.ts +8 -0
  58. package/src/contracts/testTriggerTypes.ts +66 -0
  59. package/src/contracts/workflowTypes.ts +36 -7
  60. package/src/contracts.ts +59 -0
  61. package/src/events/ConnectionInvocationEventPublisher.ts +46 -0
  62. package/src/events/index.ts +1 -0
  63. package/src/events/runEvents.ts +74 -0
  64. package/src/execution/ChildExecutionScopeFactory.ts +55 -0
  65. package/src/execution/DefaultExecutionContextFactory.ts +6 -0
  66. package/src/execution/ExecutionTelemetryCostTrackingDecoratorFactory.ts +18 -0
  67. package/src/execution/NodeExecutor.ts +10 -2
  68. package/src/execution/NodeInstanceFactory.ts +13 -1
  69. package/src/execution/NodeInstantiationError.ts +16 -0
  70. package/src/execution/NodeRunStateWriter.ts +7 -0
  71. package/src/execution/NodeRunStateWriterFactory.ts +7 -0
  72. package/src/execution/WorkflowRunExecutionContextFactory.ts +3 -0
  73. package/src/execution/index.ts +2 -0
  74. package/src/index.ts +8 -0
  75. package/src/orchestration/AbortControllerFactory.ts +9 -0
  76. package/src/orchestration/NodeExecutionRequestHandlerService.ts +1 -0
  77. package/src/orchestration/RunContinuationService.ts +3 -0
  78. package/src/orchestration/RunStartService.ts +122 -3
  79. package/src/orchestration/TestSuiteOrchestrator.ts +350 -0
  80. package/src/orchestration/TestSuiteRunIdFactory.ts +11 -0
  81. package/src/orchestration/TriggerRuntimeService.ts +34 -7
  82. package/src/orchestration/index.ts +9 -0
  83. package/src/runtime/EngineFactory.ts +12 -0
  84. package/src/testing/WorkflowTestKitNodeRegistrationContextFactory.ts +1 -3
  85. package/src/triggers/polling/PollingTriggerDedupWindow.ts +23 -0
  86. package/src/triggers/polling/PollingTriggerLogger.ts +18 -0
  87. package/src/triggers/polling/PollingTriggerRuntime.ts +122 -0
  88. package/src/triggers/polling/index.ts +5 -0
  89. package/src/types/index.ts +12 -9
  90. package/src/workflow/definition/NodeIterationIdFactory.ts +26 -0
  91. package/src/workflow/dsl/NodeIdSlugifier.ts +18 -0
  92. package/src/workflow/dsl/WorkflowBuilder.ts +71 -3
  93. package/src/workflow/dsl/WorkflowDefinitionError.ts +15 -0
  94. package/src/workflow/index.ts +3 -0
  95. package/dist/InMemoryRunEventBusRegistry-B0_C4OnP.cjs +0 -262
  96. package/dist/InMemoryRunEventBusRegistry-B0_C4OnP.cjs.map +0 -1
  97. package/dist/InMemoryRunEventBusRegistry-C2U83Hmv.js +0 -238
  98. package/dist/InMemoryRunEventBusRegistry-C2U83Hmv.js.map +0 -1
  99. package/dist/bootstrap-BaN6hZ5I.cjs.map +0 -1
  100. package/dist/bootstrap-d_BMaDT4.js +0 -221
  101. package/dist/bootstrap-d_BMaDT4.js.map +0 -1
  102. package/dist/runtime-DUW6tIJ1.js.map +0 -1
  103. package/dist/runtime-Dvo2ru5A.cjs.map +0 -1
@@ -2,6 +2,7 @@ import { instanceCachingFactory, type DependencyContainer } from "../../di";
2
2
  import { CoreTokens } from "../../di";
3
3
  import { EngineExecutionLimitsPolicyFactory } from "../../policies/executionLimits/EngineExecutionLimitsPolicyFactory";
4
4
  import {
5
+ ChildExecutionScopeFactory,
5
6
  DefaultAsyncSleeper,
6
7
  InProcessRetryRunnerFactory,
7
8
  ItemExprResolver,
@@ -50,19 +51,25 @@ export class EngineRuntimeRegistrar {
50
51
  if (!container.isRegistered(RunnableOutputBehaviorResolver, true)) {
51
52
  container.registerSingleton(RunnableOutputBehaviorResolver, RunnableOutputBehaviorResolver);
52
53
  }
53
- container.register(EngineExecutionLimitsPolicyFactory, { useClass: EngineExecutionLimitsPolicyFactory });
54
- container.register(NodeInstanceFactoryFactory, { useClass: NodeInstanceFactoryFactory });
55
- container.register(DefaultAsyncSleeper, { useClass: DefaultAsyncSleeper });
56
- container.register(InProcessRetryRunnerFactory, { useClass: InProcessRetryRunnerFactory });
57
- container.register(NodeExecutorFactory, { useClass: NodeExecutorFactory });
58
- container.register(InlineDrivingSchedulerFactory, { useClass: InlineDrivingSchedulerFactory });
59
- container.register(RunIntentServiceFactory, { useClass: RunIntentServiceFactory });
60
- container.register(EngineWorkflowRunnerServiceFactory, {
61
- useClass: EngineWorkflowRunnerServiceFactory,
62
- });
63
- container.register(WorkflowRepositoryWebhookTriggerMatcherFactory, {
64
- useClass: WorkflowRepositoryWebhookTriggerMatcherFactory,
65
- });
54
+ if (!container.isRegistered(ChildExecutionScopeFactory, true)) {
55
+ container.register(ChildExecutionScopeFactory, {
56
+ useFactory: instanceCachingFactory((dependencyContainer) => {
57
+ return new ChildExecutionScopeFactory(dependencyContainer.resolve(CoreTokens.ActivationIdFactory));
58
+ }),
59
+ });
60
+ }
61
+ container.registerSingleton(EngineExecutionLimitsPolicyFactory, EngineExecutionLimitsPolicyFactory);
62
+ container.registerSingleton(NodeInstanceFactoryFactory, NodeInstanceFactoryFactory);
63
+ container.registerSingleton(DefaultAsyncSleeper, DefaultAsyncSleeper);
64
+ container.registerSingleton(InProcessRetryRunnerFactory, InProcessRetryRunnerFactory);
65
+ container.registerSingleton(NodeExecutorFactory, NodeExecutorFactory);
66
+ container.registerSingleton(InlineDrivingSchedulerFactory, InlineDrivingSchedulerFactory);
67
+ container.registerSingleton(RunIntentServiceFactory, RunIntentServiceFactory);
68
+ container.registerSingleton(EngineWorkflowRunnerServiceFactory, EngineWorkflowRunnerServiceFactory);
69
+ container.registerSingleton(
70
+ WorkflowRepositoryWebhookTriggerMatcherFactory,
71
+ WorkflowRepositoryWebhookTriggerMatcherFactory,
72
+ );
66
73
  }
67
74
 
68
75
  private registerExecutionLimitsPolicy(
@@ -133,7 +140,7 @@ export class EngineRuntimeRegistrar {
133
140
  }
134
141
 
135
142
  private registerEngine(container: DependencyContainer, options: EngineRuntimeRegistrationOptions | undefined): void {
136
- container.register(EngineFactory, { useClass: EngineFactory });
143
+ container.registerSingleton(EngineFactory, EngineFactory);
137
144
  const matcherProvider = this.resolveMatcherProvider(options);
138
145
  container.register(Engine, {
139
146
  useFactory: instanceCachingFactory((dependencyContainer) => {
package/src/browser.ts CHANGED
@@ -9,6 +9,7 @@ export type {
9
9
  } from "./ai/AgentConnectionNodeCollector";
10
10
  export type { AgentNodeConfig } from "./ai/AiHost";
11
11
  export { ConnectionNodeIdFactory } from "./workflow/definition/ConnectionNodeIdFactory";
12
+ export { NodeIterationIdFactory } from "./workflow/definition/NodeIterationIdFactory";
12
13
  export * from "./contracts/credentialTypes";
13
14
  export * from "./contracts/runtimeTypes";
14
15
  export * from "./contracts/runFinishedAtFactory";
@@ -9,4 +9,10 @@ export class CodemationTelemetryAttributeNames {
9
9
  static readonly connectionInvocationId = "codemation.connection.invocation_id";
10
10
  static readonly toolName = "codemation.tool.name";
11
11
  static readonly traceParentRunId = "codemation.parent.run.id";
12
+ /** Per-item iteration that emitted this span/metric. Set on spans recorded inside a runnable per-item loop. */
13
+ static readonly iterationId = "codemation.iteration.id";
14
+ /** Item index (0-based) of the iteration. */
15
+ static readonly iterationIndex = "codemation.iteration.index";
16
+ /** Set when this span/metric was recorded under a sub-agent triggered by an outer LLM/tool call. */
17
+ static readonly parentInvocationId = "codemation.parent.invocation_id";
12
18
  }
@@ -1,15 +1,6 @@
1
- import type { NodeActivationId, NodeId } from "./workflowTypes";
2
- import type { NodeExecutionTelemetry, TelemetryChildSpanStart, TelemetrySpanScope } from "./telemetryTypes";
1
+ import type { NodeExecutionTelemetry } from "./telemetryTypes";
3
2
  import { NoOpTelemetrySpanScope } from "./NoOpTelemetrySpanScope";
4
3
 
5
4
  export class NoOpNodeExecutionTelemetry {
6
- static readonly value: NodeExecutionTelemetry = {
7
- ...NoOpTelemetrySpanScope.value,
8
- forNode(_: Readonly<{ nodeId: NodeId; activationId: NodeActivationId }>): NodeExecutionTelemetry {
9
- return NoOpNodeExecutionTelemetry.value;
10
- },
11
- startChildSpan(_: TelemetryChildSpanStart): TelemetrySpanScope {
12
- return NoOpTelemetrySpanScope.value;
13
- },
14
- };
5
+ static readonly value: NodeExecutionTelemetry = NoOpTelemetrySpanScope.nodeExecutionTelemetryValue;
15
6
  }
@@ -1,6 +1,9 @@
1
+ import type { NodeActivationId, NodeId } from "./workflowTypes";
1
2
  import type {
3
+ NodeExecutionTelemetry,
2
4
  TelemetryArtifactAttachment,
3
5
  TelemetryArtifactReference,
6
+ TelemetryChildSpanStart,
4
7
  TelemetryMetricRecord,
5
8
  TelemetrySpanEnd,
6
9
  TelemetrySpanEventRecord,
@@ -8,15 +11,48 @@ import type {
8
11
  } from "./telemetryTypes";
9
12
  import { NoOpTelemetryArtifactReference } from "./NoOpTelemetryArtifactReference";
10
13
 
14
+ /**
15
+ * Standalone no-op {@link NodeExecutionTelemetry} value used as the return for `asNodeTelemetry`.
16
+ *
17
+ * Defined here (instead of in `NoOpNodeExecutionTelemetry.ts`) so that {@link NoOpTelemetrySpanScope}
18
+ * can return it without importing the other module — both no-ops share this leaf.
19
+ */
20
+ const noOpNodeExecutionTelemetry: NodeExecutionTelemetry = {
21
+ traceId: "00000000000000000000000000000000",
22
+ spanId: "0000000000000000",
23
+ addSpanEvent(_: TelemetrySpanEventRecord): void {},
24
+ recordMetric(_: TelemetryMetricRecord): void {},
25
+ attachArtifact(_: TelemetryArtifactAttachment): TelemetryArtifactReference {
26
+ return NoOpTelemetryArtifactReference.value;
27
+ },
28
+ end(_: TelemetrySpanEnd = {}): void {},
29
+ asNodeTelemetry(_: Readonly<{ nodeId: NodeId; activationId: NodeActivationId }>): NodeExecutionTelemetry {
30
+ return noOpNodeExecutionTelemetry;
31
+ },
32
+ forNode(_: Readonly<{ nodeId: NodeId; activationId: NodeActivationId }>): NodeExecutionTelemetry {
33
+ return noOpNodeExecutionTelemetry;
34
+ },
35
+ startChildSpan(_: TelemetryChildSpanStart): TelemetrySpanScope {
36
+ return noOpTelemetrySpanScope;
37
+ },
38
+ };
39
+
40
+ const noOpTelemetrySpanScope: TelemetrySpanScope = {
41
+ traceId: "00000000000000000000000000000000",
42
+ spanId: "0000000000000000",
43
+ addSpanEvent(_: TelemetrySpanEventRecord): void {},
44
+ recordMetric(_: TelemetryMetricRecord): void {},
45
+ attachArtifact(_: TelemetryArtifactAttachment): TelemetryArtifactReference {
46
+ return NoOpTelemetryArtifactReference.value;
47
+ },
48
+ end(_: TelemetrySpanEnd = {}): void {},
49
+ asNodeTelemetry(_: Readonly<{ nodeId: NodeId; activationId: NodeActivationId }>): NodeExecutionTelemetry {
50
+ return noOpNodeExecutionTelemetry;
51
+ },
52
+ };
53
+
11
54
  export class NoOpTelemetrySpanScope {
12
- static readonly value: TelemetrySpanScope = {
13
- traceId: "00000000000000000000000000000000",
14
- spanId: "0000000000000000",
15
- addSpanEvent(_: TelemetrySpanEventRecord): void {},
16
- recordMetric(_: TelemetryMetricRecord): void {},
17
- attachArtifact(_: TelemetryArtifactAttachment): TelemetryArtifactReference {
18
- return NoOpTelemetryArtifactReference.value;
19
- },
20
- end(_: TelemetrySpanEnd = {}): void {},
21
- };
55
+ static readonly value: TelemetrySpanScope = noOpTelemetrySpanScope;
56
+ /** Internal: the shared no-op {@link NodeExecutionTelemetry} that {@link NoOpNodeExecutionTelemetry} re-exposes. */
57
+ static readonly nodeExecutionTelemetryValue: NodeExecutionTelemetry = noOpNodeExecutionTelemetry;
22
58
  }
@@ -0,0 +1,63 @@
1
+ import type { JsonValue, NodeId } from "./workflowTypes";
2
+
3
+ /**
4
+ * One assertion emitted by an assertion-emitting node (a node whose config sets
5
+ * `emitsAssertions: true`). Each emitted item on `main` carries one of these as `item.json`.
6
+ *
7
+ * Pass/fail is derived from `score >= (passThreshold ?? 0.5)` — see {@link deriveAssertionPassed}.
8
+ * The `errored` marker is for cases where the assertion code itself threw (distinct from
9
+ * "the assertion was evaluated and the score was low") and is treated as a hard fail in rollups
10
+ * regardless of `score`.
11
+ */
12
+ export interface AssertionResult {
13
+ readonly name: string;
14
+ /** 0..1 score. Source of truth for pass/fail (compared against `passThreshold`). */
15
+ readonly score: number;
16
+ /** 0..1 threshold for "passed". When omitted, consumers default to 0.5. */
17
+ readonly passThreshold?: number;
18
+ /** True when evaluating the assertion threw — treated as fail regardless of `score`. */
19
+ readonly errored?: true;
20
+ /** What the assertion expected. Free-form JSON; UIs render with a JSON viewer. */
21
+ readonly expected?: JsonValue;
22
+ /** What the workflow actually produced. */
23
+ readonly actual?: JsonValue;
24
+ /** Short human-readable explanation, especially for fails / errors. */
25
+ readonly message?: string;
26
+ /** Bag of supplemental fields (e.g. judge prompt, judge raw response, comparison method). */
27
+ readonly details?: Readonly<Record<string, JsonValue>>;
28
+ }
29
+
30
+ /**
31
+ * Default {@link AssertionResult.passThreshold} when authors omit it. Boolean-style assertions
32
+ * (assertEqual / contains / etc.) emit `score: 1` or `score: 0` so this default works for them;
33
+ * AI-judge assertions are expected to set their own threshold.
34
+ */
35
+ export const DEFAULT_ASSERTION_PASS_THRESHOLD = 0.5;
36
+
37
+ /**
38
+ * Derive whether an assertion result is considered "passing" using the score-based contract:
39
+ * `errored` always fails, otherwise `score >= (passThreshold ?? 0.5)`. This is the canonical
40
+ * derivation — UI and rollup code should call it rather than inlining the comparison so future
41
+ * tweaks (e.g. NaN handling) land in one place.
42
+ */
43
+ export function deriveAssertionPassed(result: {
44
+ readonly score: number;
45
+ readonly passThreshold?: number;
46
+ readonly errored?: true;
47
+ }): boolean {
48
+ if (result.errored === true) return false;
49
+ const threshold = result.passThreshold ?? DEFAULT_ASSERTION_PASS_THRESHOLD;
50
+ return result.score >= threshold;
51
+ }
52
+
53
+ /**
54
+ * Provenance for a persisted {@link AssertionResult}: which node produced it and where in the
55
+ * per-item iteration tree it landed. Filled in by the host-side persister, not the node itself.
56
+ */
57
+ export interface AssertionResultProvenance {
58
+ readonly nodeId: NodeId;
59
+ /** Per-item iteration id when the emitting node ran inside a per-item loop. */
60
+ readonly iterationId?: string;
61
+ /** Item index (0-based) within the activation that produced this assertion. */
62
+ readonly itemIndex?: number;
63
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Minimal base types that have no dependencies on other contracts.
3
+ * Used by credentialTypes, workflowTypes, and other contract layers
4
+ * to avoid circular dependencies.
5
+ */
6
+
7
+ export type WorkflowId = string;
8
+ export type NodeId = string;
9
+ export type OutputPortKey = string;
10
+ export type InputPortKey = string;
11
+ export type PersistedTokenId = string;
12
+ export type NodeConnectionName = string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Represents a typed store for a single collection.
3
+ * All rows include auto-managed id, created_at, and updated_at fields.
4
+ */
5
+ export interface CollectionStore<TRow extends Record<string, unknown> = Record<string, unknown>> {
6
+ /**
7
+ * Insert a new row. id, created_at, and updated_at are auto-populated.
8
+ */
9
+ insert(row: TRow): Promise<TRow & { id: string; created_at: Date; updated_at: Date }>;
10
+
11
+ /**
12
+ * Get a single row by id.
13
+ */
14
+ get(id: string): Promise<(TRow & { id: string; created_at: Date; updated_at: Date }) | null>;
15
+
16
+ /**
17
+ * Find a single row matching the provided filter.
18
+ */
19
+ findOne(filter: Partial<TRow>): Promise<(TRow & { id: string; created_at: Date; updated_at: Date }) | null>;
20
+
21
+ /**
22
+ * List rows with optional pagination and filtering.
23
+ */
24
+ list(opts?: {
25
+ limit?: number;
26
+ offset?: number;
27
+ where?: Partial<TRow>;
28
+ }): Promise<{ rows: ReadonlyArray<TRow & { id: string; created_at: Date; updated_at: Date }>; total: number }>;
29
+
30
+ /**
31
+ * Update a row by id with partial data.
32
+ */
33
+ update(id: string, patch: Partial<TRow>): Promise<TRow & { id: string; created_at: Date; updated_at: Date }>;
34
+
35
+ /**
36
+ * Delete a row by id. Hard delete only (no soft delete).
37
+ */
38
+ delete(id: string): Promise<{ deleted: boolean }>;
39
+ }
40
+
41
+ /**
42
+ * Runtime collections context: keyed by collection name.
43
+ */
44
+ export type CollectionsContext = Readonly<Record<string, CollectionStore>>;
@@ -1,4 +1,4 @@
1
- import type { NodeId, WorkflowId } from "./workflowTypes";
1
+ import type { NodeId, WorkflowId } from "./baseTypes";
2
2
 
3
3
  export type CredentialTypeId = string;
4
4
  export type CredentialInstanceId = string;
@@ -91,6 +91,28 @@ export type CredentialOAuth2AuthDefinition = Readonly<
91
91
  clientIdFieldKey?: string;
92
92
  clientSecretFieldKey?: string;
93
93
  }
94
+ | {
95
+ kind: "oauth2";
96
+ /**
97
+ * Free-form provider identifier for telemetry, DB rows, and Better Auth provider naming.
98
+ * Not used for any registry lookup — URLs come from {@link authorizeUrl} / {@link tokenUrl}.
99
+ */
100
+ providerId: string;
101
+ /**
102
+ * Authorization endpoint. May contain `{publicFieldKey}` placeholders that the runtime
103
+ * substitutes from the credential's resolved public config (URL-encoded).
104
+ * Example: `https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize`
105
+ */
106
+ authorizeUrl: string;
107
+ /** Token endpoint. Same templating rules as {@link authorizeUrl}. */
108
+ tokenUrl: string;
109
+ /** Optional userinfo endpoint. Same templating rules as {@link authorizeUrl}. */
110
+ userInfoUrl?: string;
111
+ scopes: ReadonlyArray<string>;
112
+ scopesFromPublicConfig?: CredentialOAuth2ScopesFromPublicConfig;
113
+ clientIdFieldKey?: string;
114
+ clientSecretFieldKey?: string;
115
+ }
94
116
  >;
95
117
 
96
118
  export type CredentialAuthDefinition = CredentialOAuth2AuthDefinition;
@@ -148,6 +148,30 @@ export interface WorkflowRunDetailDto {
148
148
  readonly mutableState?: PersistedMutableRunState;
149
149
  readonly slotStates: ReadonlyArray<SlotExecutionStateDto>;
150
150
  readonly executionInstances: ReadonlyArray<ExecutionInstanceDto>;
151
+ readonly iterations?: ReadonlyArray<RunIterationDto>;
152
+ }
153
+
154
+ /**
155
+ * Per-item iteration projected from connection invocations and node activations.
156
+ *
157
+ * One iteration = one item processed by an agent within an activation. Multiple invocations
158
+ * (LLM rounds, tool calls) belonging to the same iteration share the iterationId.
159
+ */
160
+ export interface RunIterationDto {
161
+ readonly iterationId: string;
162
+ readonly agentNodeId: NodeId;
163
+ readonly activationId: NodeActivationId;
164
+ readonly itemIndex: number;
165
+ readonly itemSummary?: string;
166
+ readonly status: NodeExecutionStatus;
167
+ readonly startedAt?: string;
168
+ readonly finishedAt?: string;
169
+ readonly invocationIds: ReadonlyArray<string>;
170
+ readonly parentInvocationId?: string;
171
+ /** Estimated cost rolled up from telemetry cost metric points, keyed by ISO currency code (e.g. "USD"). Values are minor units (cents-of-cents per the metric's `cost.currency_scale`). */
172
+ readonly estimatedCostMinorByCurrency?: Readonly<Record<string, number>>;
173
+ /** Currency scale (denominator) per currency, when present on the metric points. Joined with `estimatedCostMinorByCurrency` to format human-readable amounts. */
174
+ readonly estimatedCostCurrencyScaleByCurrency?: Readonly<Record<string, number>>;
151
175
  }
152
176
 
153
177
  export interface SlotExecutionStateDto {
@@ -178,6 +202,12 @@ export interface ExecutionInstanceDto {
178
202
  readonly inputJson?: JsonValue;
179
203
  readonly outputJson?: JsonValue;
180
204
  readonly error?: Readonly<NodeExecutionError>;
205
+ /** Per-item iteration that produced this instance. Set on connectionInvocation rows produced inside per-item runnable loops. */
206
+ readonly iterationId?: string;
207
+ /** Item index (0-based) of the iteration. */
208
+ readonly itemIndex?: number;
209
+ /** Parent invocation id when this instance was emitted by a sub-agent triggered by an outer LLM/tool call. */
210
+ readonly parentInvocationId?: string;
181
211
  }
182
212
 
183
213
  export interface WorkflowDetailSelectionState {
@@ -1,3 +1,6 @@
1
+ export * from "./baseTypes";
2
+ export * from "./assertionTypes";
3
+ export * from "./collectionTypes";
1
4
  export * from "./credentialTypes";
2
5
  export * from "./emitPorts";
3
6
  export * from "./executionPersistenceContracts";
@@ -6,6 +9,7 @@ export * from "./params";
6
9
  export * from "./itemExpr";
7
10
  export * from "./runtimeTypes";
8
11
  export * from "./telemetryTypes";
12
+ export * from "./testTriggerTypes";
9
13
  export * from "./runFinishedAtFactory";
10
14
  export * from "./runTypes";
11
15
  export * from "./webhookTypes";
@@ -1,5 +1,5 @@
1
1
  import type { TypeToken } from "../di";
2
- import type { RunEventBus } from "../events/runEvents";
2
+ import type { RunEventBus, TestCaseRunStatus } from "../events/runEvents";
3
3
  import type {
4
4
  Edge,
5
5
  InputPortKey,
@@ -7,6 +7,7 @@ import type {
7
7
  JsonValue,
8
8
  NodeActivationId,
9
9
  NodeId,
10
+ NodeIterationId,
10
11
  NodeKind,
11
12
  NodeOutputs,
12
13
  OutputPortKey,
@@ -19,6 +20,23 @@ import type {
19
20
  WorkflowNodeConnection,
20
21
  } from "./workflowTypes";
21
22
 
23
+ /**
24
+ * Test-suite linkage for a run. When set, this run was started by a TestSuiteOrchestrator
25
+ * as one test case inside a TestSuiteRun. The `IsTestRun` node and host-side persisters key
26
+ * off the presence of this field. Subworkflow runs inherit it from their parent run.
27
+ */
28
+ export interface RunTestContext {
29
+ readonly testSuiteRunId: string;
30
+ readonly testCaseIndex: number;
31
+ /**
32
+ * Optional human-friendly label for this test case (e.g. an email subject when fixtures
33
+ * are loaded from a mailbox). Resolved per item by `TestTrigger.caseLabel(item)` if set,
34
+ * persisted on `Run.test_case_label` so the Tests-tab tree-table can show "RFQ for batch 14"
35
+ * instead of "run_1777755971399_bbb86beac1396".
36
+ */
37
+ readonly testCaseLabel?: string;
38
+ }
39
+
22
40
  export interface RunExecutionOptions {
23
41
  /** Run-intent override: force the inline scheduler and bypass node-level offload decisions. */
24
42
  localOnly?: boolean;
@@ -35,6 +53,8 @@ export interface RunExecutionOptions {
35
53
  maxNodeActivations?: number;
36
54
  /** Effective cap after engine policy merge (subworkflow nesting). */
37
55
  maxSubworkflowDepth?: number;
56
+ /** Present iff started by a TestSuiteOrchestrator; propagates to subworkflow runs via {@link ParentExecutionRef.testContext}. */
57
+ testContext?: RunTestContext;
38
58
  }
39
59
 
40
60
  /** Engine-owned counters persisted with the run (worker-safe). */
@@ -154,6 +174,12 @@ export interface ConnectionInvocationRecord {
154
174
  readonly startedAt?: string;
155
175
  readonly finishedAt?: string;
156
176
  readonly updatedAt: string;
177
+ /** Per-item iteration id minted by the engine when this invocation occurred inside a runnable node's per-item loop. */
178
+ readonly iterationId?: NodeIterationId;
179
+ /** Item index (0-based) of the iteration that produced this invocation. */
180
+ readonly itemIndex?: number;
181
+ /** When set, this invocation was produced inside a sub-agent triggered by the named parent invocation. */
182
+ readonly parentInvocationId?: ConnectionInvocationId;
157
183
  }
158
184
 
159
185
  /** Arguments for appending a {@link ConnectionInvocationRecord} (engine fills run/workflow ids and timestamps). */
@@ -169,6 +195,9 @@ export type ConnectionInvocationAppendArgs = Readonly<{
169
195
  queuedAt?: string;
170
196
  startedAt?: string;
171
197
  finishedAt?: string;
198
+ iterationId?: NodeIterationId;
199
+ itemIndex?: number;
200
+ parentInvocationId?: ConnectionInvocationId;
172
201
  }>;
173
202
 
174
203
  export interface RunCurrentState {
@@ -210,6 +239,13 @@ export interface RunSummary {
210
239
  workflowId: WorkflowId;
211
240
  startedAt: string;
212
241
  status: RunStatus;
242
+ /**
243
+ * Test-case status for runs dispatched as part of a TestSuiteRun. Carries the
244
+ * assertion-rollup-corrected outcome the test orchestrator persists onto the row, so the
245
+ * executions list can show "failed" for a run whose workflow completed cleanly but whose
246
+ * assertions caught regressions. Absent for non-test runs and legacy rows.
247
+ */
248
+ testCaseStatus?: TestCaseRunStatus;
213
249
  /** ISO timestamp when the run finished (derived from node snapshots or store `updatedAt`); omit while running/pending. */
214
250
  finishedAt?: string;
215
251
  parent?: ParentExecutionRef;
@@ -2,14 +2,17 @@ import type { ReadableStream as BinaryReadableStream } from "node:stream/web";
2
2
  import type { TypeToken } from "../di";
3
3
  import type { RunEventBus } from "../events/runEvents";
4
4
  import type { CredentialSessionService } from "./credentialTypes";
5
+ import type { CollectionsContext } from "./collectionTypes";
5
6
  import type { ExecutionTelemetry, ExecutionTelemetryFactory, NodeExecutionTelemetry } from "./telemetryTypes";
6
7
  import type {
7
8
  ConnectionInvocationAppendArgs,
9
+ ConnectionInvocationId,
8
10
  NodeInputsByPort,
9
11
  PersistedWorkflowSnapshot,
10
12
  PersistedWorkflowTokenRegistryLike,
11
13
  RunExecutionOptions,
12
14
  RunResult,
15
+ RunTestContext,
13
16
  WorkflowExecutionRepository,
14
17
  } from "./runTypes";
15
18
  import type { WorkflowActivationPolicy } from "./workflowActivationPolicy";
@@ -25,6 +28,7 @@ import type {
25
28
  NodeActivationId,
26
29
  NodeConfigBase,
27
30
  NodeId,
31
+ NodeIterationId,
28
32
  NodeOutputs,
29
33
  RunnableNodeConfig,
30
34
  OutputPortKey,
@@ -154,6 +158,21 @@ export interface ExecutionContext {
154
158
  telemetry: ExecutionTelemetry;
155
159
  binary: ExecutionBinaryService;
156
160
  getCredential<TSession = unknown>(slotKey: string): Promise<TSession>;
161
+ /** Per-item iteration id, set by {@link NodeExecutor} on the ctx passed into runnable `execute`. */
162
+ iterationId?: NodeIterationId;
163
+ /** Item index (0-based) within the current activation's batch; set alongside {@link iterationId}. */
164
+ itemIndex?: number;
165
+ /** When set, this ctx is executing inside a sub-agent triggered by the named parent invocation. */
166
+ parentInvocationId?: ConnectionInvocationId;
167
+ /**
168
+ * Present iff the run was started by a TestSuiteOrchestrator. The {@link IsTestRunNode}
169
+ * branches on this; assertion-emitting nodes use it to decide whether to record results.
170
+ */
171
+ testContext?: RunTestContext;
172
+ /**
173
+ * Collections registered in the codemation config, keyed by collection name.
174
+ */
175
+ readonly collections?: CollectionsContext;
157
176
  }
158
177
 
159
178
  export interface ExecutionContextFactory {
@@ -169,6 +188,7 @@ export interface ExecutionContextFactory {
169
188
  nodeState?: NodeExecutionStatePublisher;
170
189
  telemetry?: ExecutionTelemetry;
171
190
  getCredential<TSession = unknown>(slotKey: string): Promise<TSession>;
191
+ testContext?: RunTestContext;
172
192
  }): ExecutionContext;
173
193
  }
174
194
 
@@ -180,6 +200,24 @@ export interface NodeExecutionContext<TConfig extends NodeConfigBase = NodeConfi
180
200
  binary: NodeBinaryAttachmentService;
181
201
  }
182
202
 
203
+ export interface PollingTriggerHandle {
204
+ /**
205
+ * Start the polling loop. The runtime registers its own cleanup handle so callers do not need to
206
+ * call {@link TriggerSetupContext.registerCleanup} for the loop.
207
+ * @returns The state returned by the first cycle (or `undefined` when the overlap guard fired).
208
+ */
209
+ start<TState, TItem>(args: {
210
+ intervalMs: number;
211
+ seedState?: TState;
212
+ runCycle: (cycleCtx: {
213
+ previousState: TState | undefined;
214
+ signal: AbortSignal;
215
+ }) => Promise<{ items: Items<TItem>; nextState: TState }>;
216
+ }): Promise<TState | undefined>;
217
+ /** Convenience dedup-window helper. */
218
+ readonly dedup: import("../triggers/polling/PollingTriggerDedupWindow").PollingTriggerDedupWindow;
219
+ }
220
+
183
221
  export interface TriggerSetupContext<
184
222
  TConfig extends TriggerNodeConfig<any, any> = TriggerNodeConfig<any, any>,
185
223
  TSetupState extends JsonValue | undefined = TriggerNodeSetupState<TConfig>,
@@ -189,6 +227,8 @@ export interface TriggerSetupContext<
189
227
  previousState: TSetupState;
190
228
  registerCleanup(cleanup: TriggerCleanupHandle): void;
191
229
  emit(items: Items): Promise<void>;
230
+ /** Generic polling-trigger surface. Pre-binds trigger id, emit, and registerCleanup. */
231
+ readonly polling: PollingTriggerHandle;
192
232
  }
193
233
 
194
234
  export interface TriggerTestItemsContext<
@@ -431,4 +471,6 @@ export interface EngineDeps {
431
471
  workflowPolicyRuntimeDefaults?: WorkflowPolicyRuntimeDefaults;
432
472
  /** When set, logs inactive-workflow skips at boot and trigger start/stop on activation changes. */
433
473
  triggerRuntimeDiagnostics?: TriggerRuntimeDiagnostics;
474
+ /** When set, the polling-trigger runtime uses this logger for cycle info/debug/error. */
475
+ pollingTriggerLogger?: import("../triggers/polling/PollingTriggerLogger").PollingTriggerLogger;
434
476
  }
@@ -73,6 +73,14 @@ export interface TelemetrySpanScope extends TelemetryScope {
73
73
  readonly traceId: string;
74
74
  readonly spanId: string;
75
75
  end(args?: TelemetrySpanEnd): Promise<void> | void;
76
+ /**
77
+ * Lift this span into a {@link NodeExecutionTelemetry} scoped to a different (nodeId, activationId).
78
+ * Children created via the returned telemetry's `startChildSpan` get this span as their parent.
79
+ *
80
+ * Used at the sub-agent boundary so that nested runtime telemetry parents under the agent.tool.call
81
+ * span instead of the orchestrator's node-level span.
82
+ */
83
+ asNodeTelemetry(args: Readonly<{ nodeId: NodeId; activationId: NodeActivationId }>): NodeExecutionTelemetry;
76
84
  }
77
85
 
78
86
  export interface NodeExecutionTelemetry extends ExecutionTelemetry, TelemetrySpanScope {
@@ -0,0 +1,66 @@
1
+ import type { Item, NodeId, WorkflowId } from "./workflowTypes";
2
+ import type { TriggerNodeConfig } from "./workflowTypes";
3
+
4
+ /**
5
+ * Identifier minted by the host (or in-memory test runner) for one execution of a test suite.
6
+ * One TestSuiteRun produces N child workflow runs, one per item yielded by `generateItems`.
7
+ */
8
+ export type TestSuiteRunId = string;
9
+
10
+ /**
11
+ * Setup context passed to a {@link TestTriggerNodeConfig.generateItems} callback. Distinct from
12
+ * {@link import("./runtimeTypes").TriggerSetupContext} on purpose: test triggers are not
13
+ * activated by the live trigger lifecycle (webhooks, cron, polling) and never call `emit` —
14
+ * the orchestrator pulls from the iterable they return and dispatches one run per item.
15
+ */
16
+ export interface TestTriggerSetupContext<
17
+ TConfig extends TestTriggerNodeConfig<unknown> = TestTriggerNodeConfig<unknown>,
18
+ > {
19
+ readonly workflowId: WorkflowId;
20
+ readonly nodeId: NodeId;
21
+ readonly config: TConfig;
22
+ readonly testSuiteRunId: TestSuiteRunId;
23
+ /**
24
+ * Resolves a credential session for a slot declared on this trigger's
25
+ * {@link import("./workflowTypes").NodeConfigBase.getCredentialRequirements}. Same contract as
26
+ * {@link import("./runtimeTypes").ExecutionContext.getCredential}.
27
+ */
28
+ getCredential<TSession = unknown>(slotKey: string): Promise<TSession>;
29
+ /** AbortSignal raised when the suite is cancelled — long-running pulls should bail out. */
30
+ readonly signal: AbortSignal;
31
+ }
32
+
33
+ /**
34
+ * A trigger config that emits **test cases**. Each item yielded by {@link generateItems}
35
+ * becomes one workflow run (with `executionOptions.testContext` set), so 10 yielded items
36
+ * → 10 runs marked under the same TestSuiteRun.
37
+ *
38
+ * The trigger is otherwise a normal {@link TriggerNodeConfig} (so the canvas treats it like
39
+ * any other trigger), but its `triggerKind` is `"test"` so the live activation policy skips it.
40
+ */
41
+ export interface TestTriggerNodeConfig<TOutputJson = unknown> extends TriggerNodeConfig<TOutputJson, undefined> {
42
+ readonly triggerKind: "test";
43
+ /**
44
+ * Author-supplied async iterable of items, evaluated lazily. Implementations may fetch from
45
+ * credentialed APIs, read fixture files, or yield hard-coded items. The orchestrator iterates
46
+ * and dispatches one run per item, with concurrency capped by {@link concurrency} (default 4).
47
+ */
48
+ generateItems(ctx: TestTriggerSetupContext<TestTriggerNodeConfig<TOutputJson>>): AsyncIterable<Item<TOutputJson>>;
49
+ /** Per-suite-run cap on simultaneously-executing test cases. Default: 4. */
50
+ readonly concurrency?: number;
51
+ /**
52
+ * Free-form description of where the test cases come from — surfaced in the node properties
53
+ * panel and the suite-detail header so authors revisiting the workflow six months later
54
+ * remember which mailbox / folder / fixture file the cases originate from.
55
+ *
56
+ * Example: `"All emails in the Gmail label \"test/triage-fixtures\" — 14 messages as of 2026-05-03."`
57
+ */
58
+ readonly description?: string;
59
+ /**
60
+ * Resolves a human-readable label for one yielded test case (e.g. email subject). The
61
+ * orchestrator calls this once per yielded item, persists the result on the run, and the
62
+ * Tests-tab UI uses it to render the case row instead of the opaque runId. Return
63
+ * `undefined` to fall back to "Case #N".
64
+ */
65
+ caseLabel?(item: Item<TOutputJson>): string | undefined;
66
+ }