@codemation/core 0.0.16 → 0.0.19

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.
@@ -0,0 +1,172 @@
1
+ import type {
2
+ AnyCredentialType,
3
+ CredentialFieldSchema,
4
+ CredentialHealth,
5
+ CredentialJsonRecord,
6
+ CredentialSessionFactoryArgs,
7
+ CredentialType,
8
+ CredentialTypeDefinition,
9
+ } from "../contracts/credentialTypes";
10
+ import { z } from "zod";
11
+
12
+ type MaybePromise<TValue> = TValue | Promise<TValue>;
13
+
14
+ type CredentialFieldInput = CredentialFieldSchema["type"] | Readonly<Omit<CredentialFieldSchema, "key">>;
15
+
16
+ type CredentialFieldMap<TConfig extends CredentialJsonRecord> = Readonly<
17
+ Record<keyof TConfig & string, CredentialFieldInput>
18
+ >;
19
+
20
+ type ZodObjectSchema<TConfig extends CredentialJsonRecord = CredentialJsonRecord> = z.ZodType<TConfig>;
21
+
22
+ type InferCredentialConfig<TSource> =
23
+ TSource extends z.ZodType<infer TConfig, any, any>
24
+ ? Readonly<TConfig> & CredentialJsonRecord
25
+ : TSource extends CredentialFieldMap<infer TConfig>
26
+ ? TConfig
27
+ : CredentialJsonRecord;
28
+
29
+ export interface DefineCredentialOptions<
30
+ TPublicSource extends CredentialFieldMap<any> | ZodObjectSchema<any>,
31
+ TSecretSource extends CredentialFieldMap<any> | ZodObjectSchema<any>,
32
+ TSession,
33
+ > {
34
+ readonly key: string;
35
+ readonly label: string;
36
+ readonly description?: string;
37
+ readonly public: TPublicSource;
38
+ readonly secret: TSecretSource;
39
+ readonly supportedSourceKinds?: CredentialTypeDefinition["supportedSourceKinds"];
40
+ readonly auth?: CredentialTypeDefinition["auth"];
41
+ createSession(
42
+ args: CredentialSessionFactoryArgs<InferCredentialConfig<TPublicSource>, InferCredentialConfig<TSecretSource>>,
43
+ ): MaybePromise<TSession>;
44
+ test(
45
+ args: CredentialSessionFactoryArgs<InferCredentialConfig<TPublicSource>, InferCredentialConfig<TSecretSource>>,
46
+ ): MaybePromise<CredentialHealth>;
47
+ }
48
+
49
+ export class CredentialFieldSchemaFactory {
50
+ static create<TConfig extends CredentialJsonRecord>(
51
+ source: CredentialFieldMap<TConfig> | ZodObjectSchema<TConfig>,
52
+ ): ReadonlyArray<CredentialFieldSchema> {
53
+ if (source instanceof z.ZodObject) {
54
+ return this.createFromZodObject(source);
55
+ }
56
+ return this.createFromMap(source as CredentialFieldMap<TConfig>);
57
+ }
58
+
59
+ private static createFromMap<TConfig extends CredentialJsonRecord>(
60
+ source: CredentialFieldMap<TConfig>,
61
+ ): ReadonlyArray<CredentialFieldSchema> {
62
+ return Object.entries(source).map(([key, input], index) => {
63
+ if (typeof input === "string") {
64
+ return {
65
+ key,
66
+ label: this.humanize(key),
67
+ order: index,
68
+ type: input as CredentialFieldSchema["type"],
69
+ };
70
+ }
71
+ return {
72
+ key,
73
+ order: index,
74
+ ...(input as Readonly<Omit<CredentialFieldSchema, "key">>),
75
+ };
76
+ });
77
+ }
78
+
79
+ private static createFromZodObject<_TConfig extends CredentialJsonRecord>(
80
+ source: z.ZodObject,
81
+ ): ReadonlyArray<CredentialFieldSchema> {
82
+ const shape = source.shape;
83
+ return Object.entries(shape).map(([key, schema], index) => {
84
+ const resolved = this.unwrap(schema);
85
+ return {
86
+ key,
87
+ label: this.humanize(key),
88
+ order: index,
89
+ required: this.isRequired(schema) ? true : undefined,
90
+ type: this.resolveType(resolved),
91
+ };
92
+ });
93
+ }
94
+
95
+ private static isRequired(schema: z.ZodTypeAny): boolean {
96
+ return !(schema instanceof z.ZodOptional || schema instanceof z.ZodDefault);
97
+ }
98
+
99
+ private static unwrap(schema: z.ZodTypeAny): z.ZodTypeAny {
100
+ let current: z.ZodTypeAny = schema;
101
+ while (current instanceof z.ZodOptional || current instanceof z.ZodDefault) {
102
+ current = current.unwrap() as z.ZodTypeAny;
103
+ }
104
+ return current;
105
+ }
106
+
107
+ private static resolveType(schema: z.ZodTypeAny): CredentialFieldSchema["type"] {
108
+ if (schema instanceof z.ZodBoolean) {
109
+ return "boolean";
110
+ }
111
+ if (schema instanceof z.ZodString) {
112
+ return "string";
113
+ }
114
+ return "json";
115
+ }
116
+
117
+ private static humanize(key: string): string {
118
+ return key
119
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
120
+ .replace(/[-_.]+/g, " ")
121
+ .replace(/\s+/g, " ")
122
+ .trim()
123
+ .replace(/^./, (character) => character.toUpperCase());
124
+ }
125
+ }
126
+
127
+ export function defineCredential<
128
+ TPublicSource extends CredentialFieldMap<any> | ZodObjectSchema<any>,
129
+ TSecretSource extends CredentialFieldMap<any> | ZodObjectSchema<any>,
130
+ TSession,
131
+ >(
132
+ options: DefineCredentialOptions<TPublicSource, TSecretSource, TSession>,
133
+ ): CredentialType<InferCredentialConfig<TPublicSource>, InferCredentialConfig<TSecretSource>, TSession> & {
134
+ readonly key: string;
135
+ } {
136
+ const definition: CredentialTypeDefinition = {
137
+ typeId: options.key,
138
+ displayName: options.label,
139
+ description: options.description,
140
+ publicFields: CredentialFieldSchemaFactory.create(options.public),
141
+ secretFields: CredentialFieldSchemaFactory.create(options.secret),
142
+ supportedSourceKinds: options.supportedSourceKinds ?? ["db", "env", "code"],
143
+ auth: options.auth,
144
+ };
145
+
146
+ const credentialType: AnyCredentialType = {
147
+ definition,
148
+ async createSession(args) {
149
+ return await options.createSession(
150
+ args as CredentialSessionFactoryArgs<
151
+ InferCredentialConfig<TPublicSource>,
152
+ InferCredentialConfig<TSecretSource>
153
+ >,
154
+ );
155
+ },
156
+ async test(args) {
157
+ return await options.test(
158
+ args as CredentialSessionFactoryArgs<
159
+ InferCredentialConfig<TPublicSource>,
160
+ InferCredentialConfig<TSecretSource>
161
+ >,
162
+ );
163
+ },
164
+ };
165
+
166
+ return {
167
+ ...credentialType,
168
+ key: options.key,
169
+ } as CredentialType<InferCredentialConfig<TPublicSource>, InferCredentialConfig<TSecretSource>, TSession> & {
170
+ readonly key: string;
171
+ };
172
+ }
@@ -0,0 +1,227 @@
1
+ import type {
2
+ AnyCredentialType,
3
+ CredentialJsonRecord,
4
+ CredentialRequirement,
5
+ CredentialTypeId,
6
+ } from "../contracts/credentialTypes";
7
+ import type { Node, NodeExecutionContext } from "../contracts/runtimeTypes";
8
+ import type { Items, NodeOutputs, RunnableNodeConfig } from "../contracts/workflowTypes";
9
+ import type { TypeToken } from "../di";
10
+ import { node as persistedNode } from "../runtime-types/runtimeTypeDecorators.types";
11
+ import { z } from "zod";
12
+ import { DefinedNodeRegistry } from "./DefinedNodeRegistry";
13
+
14
+ type MaybePromise<TValue> = TValue | Promise<TValue>;
15
+
16
+ type ResolvableCredentialType = AnyCredentialType | CredentialTypeId;
17
+
18
+ type SessionForCredentialType<TCredential extends ResolvableCredentialType> = TCredential extends AnyCredentialType
19
+ ? Awaited<ReturnType<TCredential["createSession"]>>
20
+ : unknown;
21
+
22
+ export type DefinedNodeCredentialBinding =
23
+ | ResolvableCredentialType
24
+ | Readonly<{
25
+ readonly type: ResolvableCredentialType | ReadonlyArray<ResolvableCredentialType>;
26
+ readonly label?: string;
27
+ readonly optional?: true;
28
+ readonly helpText?: string;
29
+ readonly helpUrl?: string;
30
+ }>;
31
+
32
+ export type DefinedNodeCredentialBindings = Readonly<Record<string, DefinedNodeCredentialBinding>>;
33
+
34
+ type SessionForBinding<TBinding extends DefinedNodeCredentialBinding> =
35
+ TBinding extends Readonly<{ type: infer TType }>
36
+ ? TType extends ReadonlyArray<infer TEntry>
37
+ ? SessionForCredentialType<TEntry & ResolvableCredentialType>
38
+ : SessionForCredentialType<TType & ResolvableCredentialType>
39
+ : SessionForCredentialType<TBinding & ResolvableCredentialType>;
40
+
41
+ export type DefinedNodeCredentialAccessors<TBindings extends DefinedNodeCredentialBindings | undefined> =
42
+ TBindings extends DefinedNodeCredentialBindings
43
+ ? Readonly<{
44
+ [TKey in keyof TBindings]: () => Promise<SessionForBinding<TBindings[TKey]>>;
45
+ }>
46
+ : Readonly<Record<string, never>>;
47
+
48
+ export interface DefinedNodeRunContext<
49
+ TConfig extends CredentialJsonRecord,
50
+ TBindings extends DefinedNodeCredentialBindings | undefined,
51
+ > {
52
+ readonly config: TConfig;
53
+ readonly credentials: DefinedNodeCredentialAccessors<TBindings>;
54
+ readonly execution: NodeExecutionContext<RunnableNodeConfig<TConfig, unknown>>;
55
+ }
56
+
57
+ export interface DefinedNode<
58
+ TKey extends string,
59
+ TConfig extends CredentialJsonRecord,
60
+ TInputJson,
61
+ TOutputJson,
62
+ _TBindings extends DefinedNodeCredentialBindings | undefined = undefined,
63
+ > {
64
+ readonly kind: "defined-node";
65
+ readonly key: TKey;
66
+ readonly title: string;
67
+ readonly description?: string;
68
+ create(config: TConfig, name?: string, id?: string): RunnableNodeConfig<TInputJson, TOutputJson>;
69
+ register(context: { registerNode<TValue>(token: TypeToken<TValue>, implementation?: TypeToken<TValue>): void }): void;
70
+ }
71
+
72
+ export interface DefineNodeOptions<
73
+ TKey extends string,
74
+ TConfig extends CredentialJsonRecord,
75
+ TInputJson,
76
+ TOutputJson,
77
+ TBindings extends DefinedNodeCredentialBindings | undefined = undefined,
78
+ > {
79
+ readonly key: TKey;
80
+ readonly title: string;
81
+ readonly description?: string;
82
+ readonly input?: Readonly<Record<keyof TConfig & string, unknown>>;
83
+ readonly configSchema?: z.ZodType<TConfig>;
84
+ readonly credentials?: TBindings;
85
+ run(
86
+ items: ReadonlyArray<TInputJson>,
87
+ context: DefinedNodeRunContext<TConfig, TBindings>,
88
+ ): MaybePromise<ReadonlyArray<TOutputJson>>;
89
+ }
90
+
91
+ const definedNodeCredentialRequirementFactory = {
92
+ create(bindings: DefinedNodeCredentialBindings | undefined): ReadonlyArray<CredentialRequirement> {
93
+ if (!bindings) {
94
+ return [];
95
+ }
96
+ return Object.entries(bindings).map(([slotKey, binding]) => {
97
+ if (typeof binding === "string" || this.isCredentialType(binding)) {
98
+ return {
99
+ slotKey,
100
+ label: this.humanize(slotKey),
101
+ acceptedTypes: [this.resolveTypeId(binding)],
102
+ };
103
+ }
104
+
105
+ const types = Array.isArray(binding.type) ? binding.type : [binding.type];
106
+ return {
107
+ slotKey,
108
+ label: binding.label ?? this.humanize(slotKey),
109
+ acceptedTypes: types.map((entry) => this.resolveTypeId(entry)),
110
+ optional: binding.optional,
111
+ helpText: binding.helpText,
112
+ helpUrl: binding.helpUrl,
113
+ };
114
+ });
115
+ },
116
+
117
+ isCredentialType(value: unknown): value is AnyCredentialType {
118
+ return (
119
+ Boolean(value) &&
120
+ typeof value === "object" &&
121
+ "definition" in (value as Record<string, unknown>) &&
122
+ typeof (value as AnyCredentialType).definition?.typeId === "string"
123
+ );
124
+ },
125
+
126
+ resolveTypeId(type: ResolvableCredentialType): string {
127
+ return typeof type === "string" ? type : type.definition.typeId;
128
+ },
129
+
130
+ humanize(key: string): string {
131
+ return key
132
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
133
+ .replace(/[-_.]+/g, " ")
134
+ .replace(/\s+/g, " ")
135
+ .trim()
136
+ .replace(/^./, (character) => character.toUpperCase());
137
+ },
138
+ } as const;
139
+
140
+ const definedNodeCredentialAccessorFactory = {
141
+ create<TBindings extends DefinedNodeCredentialBindings | undefined>(
142
+ bindings: TBindings,
143
+ ctx: NodeExecutionContext<RunnableNodeConfig<any, any>>,
144
+ ): DefinedNodeCredentialAccessors<TBindings> {
145
+ if (!bindings) {
146
+ return {} as DefinedNodeCredentialAccessors<TBindings>;
147
+ }
148
+ const entries = Object.keys(bindings).map((slotKey) => [slotKey, () => ctx.getCredential(slotKey)] as const);
149
+ return Object.fromEntries(entries) as DefinedNodeCredentialAccessors<TBindings>;
150
+ },
151
+ } as const;
152
+
153
+ export function defineNode<
154
+ TKey extends string,
155
+ TConfig extends CredentialJsonRecord,
156
+ TInputJson,
157
+ TOutputJson,
158
+ TBindings extends DefinedNodeCredentialBindings | undefined = undefined,
159
+ >(
160
+ options: DefineNodeOptions<TKey, TConfig, TInputJson, TOutputJson, TBindings>,
161
+ ): DefinedNode<TKey, TConfig, TInputJson, TOutputJson, TBindings> {
162
+ const credentialRequirements = definedNodeCredentialRequirementFactory.create(options.credentials);
163
+ type DefinedRunnableNodeConfigShape = RunnableNodeConfig<TInputJson, TOutputJson> & Readonly<{ config: TConfig }>;
164
+
165
+ const DefinedNodeRuntime = class implements Node<DefinedRunnableNodeConfigShape> {
166
+ readonly kind = "node" as const;
167
+ readonly outputPorts = ["main"] as const;
168
+
169
+ async execute(items: Items, ctx: NodeExecutionContext<DefinedRunnableNodeConfigShape>): Promise<NodeOutputs> {
170
+ const outputs = await options.run(
171
+ items.map((item) => item.json as TInputJson),
172
+ {
173
+ config: ctx.config.config,
174
+ credentials: definedNodeCredentialAccessorFactory.create(
175
+ options.credentials,
176
+ ctx,
177
+ ) as DefinedNodeCredentialAccessors<TBindings>,
178
+ execution: ctx as unknown as NodeExecutionContext<RunnableNodeConfig<TConfig, unknown>>,
179
+ },
180
+ );
181
+
182
+ return {
183
+ main: outputs.map((json, index) => {
184
+ const existing = items[index];
185
+ if (!existing) {
186
+ return { json };
187
+ }
188
+ return { ...existing, json };
189
+ }),
190
+ };
191
+ }
192
+ };
193
+
194
+ persistedNode({ name: options.key })(DefinedNodeRuntime);
195
+
196
+ const DefinedRunnableNodeConfig = class implements RunnableNodeConfig<TInputJson, TOutputJson> {
197
+ readonly kind = "node" as const;
198
+ readonly type: TypeToken<unknown> = DefinedNodeRuntime;
199
+
200
+ constructor(
201
+ public readonly name: string,
202
+ public readonly config: TConfig,
203
+ public readonly id?: string,
204
+ ) {}
205
+
206
+ getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
207
+ return credentialRequirements;
208
+ }
209
+ };
210
+
211
+ const definition: DefinedNode<TKey, TConfig, TInputJson, TOutputJson, TBindings> = {
212
+ kind: "defined-node",
213
+ key: options.key,
214
+ title: options.title,
215
+ description: options.description,
216
+ create(config, name = options.title, id) {
217
+ return new DefinedRunnableNodeConfig(name, config, id);
218
+ },
219
+ register(context) {
220
+ context.registerNode(DefinedNodeRuntime);
221
+ },
222
+ };
223
+
224
+ DefinedNodeRegistry.register(definition as DefinedNode<string, Record<string, unknown>, unknown, unknown>);
225
+
226
+ return definition;
227
+ }
@@ -0,0 +1,12 @@
1
+ export { DefinedNodeRegistry } from "./DefinedNodeRegistry";
2
+ export type {
3
+ DefinedNode,
4
+ DefinedNodeCredentialAccessors,
5
+ DefinedNodeCredentialBinding,
6
+ DefinedNodeCredentialBindings,
7
+ DefinedNodeRunContext,
8
+ DefineNodeOptions,
9
+ } from "./defineNode.types";
10
+ export { defineNode } from "./defineNode.types";
11
+ export type { DefineCredentialOptions } from "./defineCredential.types";
12
+ export { defineCredential } from "./defineCredential.types";
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { SystemClock, type Clock } from "./contracts/Clock";
2
+ export * from "./authoring";
2
3
  export * from "./ai/AiHost";
3
4
  export * from "./workflow";
4
5
  export * from "./di";
@@ -303,7 +303,17 @@ export class CurrentStateFrontierPlanner {
303
303
  if (!incomingEdge) {
304
304
  return false;
305
305
  }
306
- return this.hasOutputPort(currentState, incomingEdge.from.nodeId, incomingEdge.from.output);
306
+ if (!this.hasOutputPort(currentState, incomingEdge.from.nodeId, incomingEdge.from.output)) {
307
+ return false;
308
+ }
309
+ if (this.usesCollect(nodeId)) {
310
+ return true;
311
+ }
312
+ const items = this.resolveOutputItems(currentState, incomingEdge.from.nodeId, incomingEdge.from.output);
313
+ if (items.length > 0) {
314
+ return true;
315
+ }
316
+ return this.shouldContinueAfterEmptyOutputFromSource(incomingEdge.from.nodeId);
307
317
  }
308
318
 
309
319
  private resolveInput(currentState: RunCurrentState, nodeId: NodeId, input: InputPortKey): Items {
@@ -335,6 +345,19 @@ export class CurrentStateFrontierPlanner {
335
345
  return currentState.outputsByNode[nodeId]?.[output] ?? [];
336
346
  }
337
347
 
348
+ private usesCollect(nodeId: NodeId): boolean {
349
+ const expectedInputs = this.topology.expectedInputsByNode.get(nodeId) ?? [];
350
+ return expectedInputs.length !== 1 || expectedInputs[0] !== "in";
351
+ }
352
+
353
+ private shouldContinueAfterEmptyOutputFromSource(nodeId: NodeId): boolean {
354
+ const definition = this.topology.defsById.get(nodeId);
355
+ if (!definition) {
356
+ return false;
357
+ }
358
+ return definition.config.continueWhenEmptyOutput === true;
359
+ }
360
+
338
361
  private getPinnedOutputs(currentState: RunCurrentState, nodeId: NodeId): NodeOutputs | undefined {
339
362
  return currentState.mutableState?.nodesById?.[nodeId]?.pinnedOutputsByPort;
340
363
  }
@@ -20,6 +20,7 @@ export type StartWorkflowIntent = {
20
20
  workflow: WorkflowDefinition;
21
21
  startAt?: string;
22
22
  items: Items;
23
+ synthesizeTriggerItems?: boolean;
23
24
  parent?: CurrentStateExecutionRequest["parent"];
24
25
  executionOptions?: RunExecutionOptions;
25
26
  workflowSnapshot?: CurrentStateExecutionRequest["workflowSnapshot"];
@@ -34,6 +35,7 @@ export type RerunFromNodeIntent = {
34
35
  nodeId: NodeId;
35
36
  currentState: RunCurrentState;
36
37
  items?: Items;
38
+ synthesizeTriggerItems?: boolean;
37
39
  parent?: CurrentStateExecutionRequest["parent"];
38
40
  executionOptions?: RunExecutionOptions;
39
41
  workflowSnapshot?: CurrentStateExecutionRequest["workflowSnapshot"];
@@ -58,22 +60,16 @@ export class RunIntentService {
58
60
  ) {}
59
61
 
60
62
  async startWorkflow(args: StartWorkflowIntent): Promise<RunResult> {
63
+ const items = await this.resolveStartWorkflowItems(args);
61
64
  if (args.startAt && !args.currentState && !args.stopCondition && !args.reset) {
62
- return await this.engine.runWorkflow(
63
- args.workflow,
64
- args.startAt,
65
- args.items,
66
- args.parent,
67
- args.executionOptions,
68
- {
69
- workflowSnapshot: args.workflowSnapshot,
70
- mutableState: args.mutableState,
71
- },
72
- );
65
+ return await this.engine.runWorkflow(args.workflow, args.startAt, items, args.parent, args.executionOptions, {
66
+ workflowSnapshot: args.workflowSnapshot,
67
+ mutableState: args.mutableState,
68
+ });
73
69
  }
74
70
  return await this.engine.runWorkflowFromState({
75
71
  workflow: args.workflow,
76
- items: args.items,
72
+ items,
77
73
  parent: args.parent,
78
74
  executionOptions: args.executionOptions,
79
75
  workflowSnapshot: args.workflowSnapshot,
@@ -85,8 +81,9 @@ export class RunIntentService {
85
81
  }
86
82
 
87
83
  async rerunFromNode(args: RerunFromNodeIntent): Promise<RunResult> {
88
- if (args.items) {
89
- return await this.engine.runWorkflow(args.workflow, args.nodeId, args.items, args.parent, args.executionOptions, {
84
+ const items = await this.resolveRerunItems(args);
85
+ if (items) {
86
+ return await this.engine.runWorkflow(args.workflow, args.nodeId, items, args.parent, args.executionOptions, {
90
87
  workflowSnapshot: args.workflowSnapshot,
91
88
  mutableState: args.mutableState,
92
89
  });
@@ -103,6 +100,63 @@ export class RunIntentService {
103
100
  });
104
101
  }
105
102
 
103
+ private async resolveStartWorkflowItems(args: StartWorkflowIntent): Promise<Items> {
104
+ if (this.hasNonEmptyItems(args.items)) {
105
+ return args.items;
106
+ }
107
+ const triggerNodeId = this.resolveStartWorkflowTriggerNodeId(args);
108
+ if (!triggerNodeId) {
109
+ return args.items;
110
+ }
111
+ return (await this.engine.createTriggerTestItems({ workflow: args.workflow, nodeId: triggerNodeId })) ?? args.items;
112
+ }
113
+
114
+ private async resolveRerunItems(args: RerunFromNodeIntent): Promise<Items | undefined> {
115
+ if (this.hasNonEmptyItems(args.items)) {
116
+ return args.items;
117
+ }
118
+ const triggerNodeId = this.resolveRerunTriggerNodeId(args);
119
+ if (!triggerNodeId) {
120
+ return args.items;
121
+ }
122
+ return (await this.engine.createTriggerTestItems({ workflow: args.workflow, nodeId: triggerNodeId })) ?? args.items;
123
+ }
124
+
125
+ private resolveStartWorkflowTriggerNodeId(args: StartWorkflowIntent): NodeId | undefined {
126
+ if (args.stopCondition?.kind === "nodeCompleted" && this.isTriggerNode(args.workflow, args.stopCondition.nodeId)) {
127
+ return args.stopCondition.nodeId;
128
+ }
129
+ if (!args.synthesizeTriggerItems) {
130
+ return undefined;
131
+ }
132
+ if (args.startAt && this.isTriggerNode(args.workflow, args.startAt)) {
133
+ return args.startAt;
134
+ }
135
+ return this.firstTriggerNodeId(args.workflow);
136
+ }
137
+
138
+ private resolveRerunTriggerNodeId(args: RerunFromNodeIntent): NodeId | undefined {
139
+ if (this.isTriggerNode(args.workflow, args.nodeId)) {
140
+ return args.nodeId;
141
+ }
142
+ if (!args.synthesizeTriggerItems) {
143
+ return undefined;
144
+ }
145
+ return this.firstTriggerNodeId(args.workflow);
146
+ }
147
+
148
+ private firstTriggerNodeId(workflow: WorkflowDefinition): NodeId | undefined {
149
+ return workflow.nodes.find((node) => node.kind === "trigger")?.id;
150
+ }
151
+
152
+ private isTriggerNode(workflow: WorkflowDefinition, nodeId: string): boolean {
153
+ return workflow.nodes.find((node) => node.id === nodeId)?.kind === "trigger";
154
+ }
155
+
156
+ private hasNonEmptyItems(items: Items | undefined): boolean {
157
+ return (items?.length ?? 0) > 0;
158
+ }
159
+
106
160
  resolveWebhookTrigger(args: { endpointPath: string; method: HttpMethod }): WebhookTriggerResolution {
107
161
  return this.engine.resolveWebhookTrigger(args);
108
162
  }