@hexabot-ai/agentic 3.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +120 -0
- package/README.md +231 -0
- package/dist/cjs/action/abstract-action.js +141 -0
- package/dist/cjs/action/action.js +47 -0
- package/dist/cjs/action/action.types.js +7 -0
- package/dist/cjs/bindings/base-binding.js +278 -0
- package/dist/cjs/context.js +79 -0
- package/dist/cjs/dsl.types.js +318 -0
- package/dist/cjs/index.js +52 -0
- package/dist/cjs/runner-runtime-control.js +343 -0
- package/dist/cjs/step-executors/conditional-executor.js +63 -0
- package/dist/cjs/step-executors/loop-executor.js +187 -0
- package/dist/cjs/step-executors/parallel-executor.js +51 -0
- package/dist/cjs/step-executors/skip-helpers.js +29 -0
- package/dist/cjs/step-executors/task-executor.js +155 -0
- package/dist/cjs/step-executors/types.js +7 -0
- package/dist/cjs/suspension-rebuilder.js +301 -0
- package/dist/cjs/utils/deferred.js +17 -0
- package/dist/cjs/utils/naming.js +48 -0
- package/dist/cjs/utils/timeout.js +42 -0
- package/dist/cjs/utils/workflow-definition.js +102 -0
- package/dist/cjs/workflow-compiler.js +212 -0
- package/dist/cjs/workflow-event-emitter.js +40 -0
- package/dist/cjs/workflow-runner.js +448 -0
- package/dist/cjs/workflow-types.js +7 -0
- package/dist/cjs/workflow-values.js +99 -0
- package/dist/cjs/workflow.js +243 -0
- package/dist/esm/action/abstract-action.js +137 -0
- package/dist/esm/action/action.js +44 -0
- package/dist/esm/action/action.types.js +6 -0
- package/dist/esm/bindings/base-binding.js +273 -0
- package/dist/esm/context.js +75 -0
- package/dist/esm/dsl.types.js +309 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/runner-runtime-control.js +338 -0
- package/dist/esm/step-executors/conditional-executor.js +60 -0
- package/dist/esm/step-executors/loop-executor.js +182 -0
- package/dist/esm/step-executors/parallel-executor.js +48 -0
- package/dist/esm/step-executors/skip-helpers.js +25 -0
- package/dist/esm/step-executors/task-executor.js +152 -0
- package/dist/esm/step-executors/types.js +6 -0
- package/dist/esm/suspension-rebuilder.js +296 -0
- package/dist/esm/utils/deferred.js +14 -0
- package/dist/esm/utils/naming.js +42 -0
- package/dist/esm/utils/timeout.js +37 -0
- package/dist/esm/utils/workflow-definition.js +98 -0
- package/dist/esm/workflow-compiler.js +208 -0
- package/dist/esm/workflow-event-emitter.js +36 -0
- package/dist/esm/workflow-runner.js +444 -0
- package/dist/esm/workflow-types.js +6 -0
- package/dist/esm/workflow-values.js +89 -0
- package/dist/esm/workflow.js +236 -0
- package/dist/types/action/abstract-action.d.ts +54 -0
- package/dist/types/action/abstract-action.d.ts.map +1 -0
- package/dist/types/action/action.d.ts +23 -0
- package/dist/types/action/action.d.ts.map +1 -0
- package/dist/types/action/action.types.d.ts +55 -0
- package/dist/types/action/action.types.d.ts.map +1 -0
- package/dist/types/bindings/base-binding.d.ts +52 -0
- package/dist/types/bindings/base-binding.d.ts.map +1 -0
- package/dist/types/context.d.ts +105 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/dsl.types.d.ts +220 -0
- package/dist/types/dsl.types.d.ts.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/runner-runtime-control.d.ts +64 -0
- package/dist/types/runner-runtime-control.d.ts.map +1 -0
- package/dist/types/step-executors/conditional-executor.d.ts +13 -0
- package/dist/types/step-executors/conditional-executor.d.ts.map +1 -0
- package/dist/types/step-executors/loop-executor.d.ts +33 -0
- package/dist/types/step-executors/loop-executor.d.ts.map +1 -0
- package/dist/types/step-executors/parallel-executor.d.ts +14 -0
- package/dist/types/step-executors/parallel-executor.d.ts.map +1 -0
- package/dist/types/step-executors/skip-helpers.d.ts +12 -0
- package/dist/types/step-executors/skip-helpers.d.ts.map +1 -0
- package/dist/types/step-executors/task-executor.d.ts +13 -0
- package/dist/types/step-executors/task-executor.d.ts.map +1 -0
- package/dist/types/step-executors/types.d.ts +24 -0
- package/dist/types/step-executors/types.d.ts.map +1 -0
- package/dist/types/suspension-rebuilder.d.ts +67 -0
- package/dist/types/suspension-rebuilder.d.ts.map +1 -0
- package/dist/types/utils/deferred.d.ts +7 -0
- package/dist/types/utils/deferred.d.ts.map +1 -0
- package/dist/types/utils/naming.d.ts +24 -0
- package/dist/types/utils/naming.d.ts.map +1 -0
- package/dist/types/utils/timeout.d.ts +17 -0
- package/dist/types/utils/timeout.d.ts.map +1 -0
- package/dist/types/utils/workflow-definition.d.ts +3 -0
- package/dist/types/utils/workflow-definition.d.ts.map +1 -0
- package/dist/types/workflow-compiler.d.ts +12 -0
- package/dist/types/workflow-compiler.d.ts.map +1 -0
- package/dist/types/workflow-event-emitter.d.ts +70 -0
- package/dist/types/workflow-event-emitter.d.ts.map +1 -0
- package/dist/types/workflow-runner.d.ts +179 -0
- package/dist/types/workflow-runner.d.ts.map +1 -0
- package/dist/types/workflow-types.d.ts +195 -0
- package/dist/types/workflow-types.d.ts.map +1 -0
- package/dist/types/workflow-values.d.ts +34 -0
- package/dist/types/workflow-values.d.ts.map +1 -0
- package/dist/types/workflow.d.ts +75 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/examples/defs-bindings-agent/actions/ai-agent.ts +110 -0
- package/examples/defs-bindings-agent/actions/caculate-score.ts +49 -0
- package/examples/defs-bindings-agent/actions/index.ts +15 -0
- package/examples/defs-bindings-agent/bindings.ts +20 -0
- package/examples/defs-bindings-agent/context.ts +32 -0
- package/examples/defs-bindings-agent/workflow.ts +67 -0
- package/examples/defs-bindings-agent/workflow.yml +28 -0
- package/examples/full/actions/await-user-input.ts +46 -0
- package/examples/full/actions/call-llm.ts +133 -0
- package/examples/full/actions/create-ticket.ts +46 -0
- package/examples/full/actions/decision-router.ts +83 -0
- package/examples/full/actions/get-calendar-events.ts +55 -0
- package/examples/full/actions/get-user-profile.ts +72 -0
- package/examples/full/actions/index.ts +33 -0
- package/examples/full/actions/query-memory.ts +45 -0
- package/examples/full/actions/search-web.ts +53 -0
- package/examples/full/actions/send-email.ts +50 -0
- package/examples/full/context.ts +41 -0
- package/examples/full/workflow.ts +75 -0
- package/examples/full/workflow.yml +359 -0
- package/examples/loop/actions/await-reply.ts +47 -0
- package/examples/loop/actions/index.ts +19 -0
- package/examples/loop/actions/send-text-message.ts +40 -0
- package/examples/loop/context.ts +32 -0
- package/examples/loop/workflow.ts +66 -0
- package/examples/loop/workflow.yml +66 -0
- package/examples/suspend-resume/actions/format-reply.ts +43 -0
- package/examples/suspend-resume/actions/index.ts +13 -0
- package/examples/suspend-resume/actions/wait-for-user.ts +51 -0
- package/examples/suspend-resume/context.ts +32 -0
- package/examples/suspend-resume/workflow.ts +79 -0
- package/examples/suspend-resume/workflow.yml +29 -0
- package/package.json +60 -0
- package/src/__tests__/parser.test.ts +95 -0
- package/src/__tests__/suspension-rebuilder.test.ts +361 -0
- package/src/__tests__/test-helpers.ts +36 -0
- package/src/__tests__/validation.test.ts +526 -0
- package/src/__tests__/workflow-compiler.test.ts +715 -0
- package/src/__tests__/workflow-definition-path.test.ts +381 -0
- package/src/__tests__/workflow-event-emitter.test.ts +50 -0
- package/src/__tests__/workflow-runner.test.ts +1397 -0
- package/src/__tests__/workflow-values.test.ts +132 -0
- package/src/__tests__/workflow.test.ts +320 -0
- package/src/action/__tests__/abstract-action-timing.test.ts +183 -0
- package/src/action/__tests__/action.test.ts +473 -0
- package/src/action/abstract-action.ts +215 -0
- package/src/action/action.ts +83 -0
- package/src/action/action.types.ts +93 -0
- package/src/bindings/base-binding.ts +508 -0
- package/src/context.ts +179 -0
- package/src/dsl.types.ts +473 -0
- package/src/index.ts +114 -0
- package/src/runner-runtime-control.ts +544 -0
- package/src/step-executors/conditional-executor.test.ts +192 -0
- package/src/step-executors/conditional-executor.ts +83 -0
- package/src/step-executors/loop-executor.test.ts +303 -0
- package/src/step-executors/loop-executor.ts +299 -0
- package/src/step-executors/parallel-executor.test.ts +182 -0
- package/src/step-executors/parallel-executor.ts +77 -0
- package/src/step-executors/skip-helpers.ts +68 -0
- package/src/step-executors/task-executor.test.ts +257 -0
- package/src/step-executors/task-executor.ts +248 -0
- package/src/step-executors/types.ts +70 -0
- package/src/suspension-rebuilder.ts +504 -0
- package/src/utils/deferred.ts +23 -0
- package/src/utils/naming.ts +55 -0
- package/src/utils/timeout.ts +48 -0
- package/src/utils/workflow-definition.ts +173 -0
- package/src/workflow-compiler.ts +317 -0
- package/src/workflow-event-emitter.ts +86 -0
- package/src/workflow-runner.ts +603 -0
- package/src/workflow-types.ts +207 -0
- package/src/workflow-values.ts +159 -0
- package/src/workflow.ts +387 -0
package/src/context.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
EventEmitterLike,
|
|
9
|
+
WorkflowEventEmitterLike,
|
|
10
|
+
} from './workflow-event-emitter';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lifecycle of a workflow run:
|
|
14
|
+
* - idle: runner constructed but `start` not invoked.
|
|
15
|
+
* - running: set when `start` or `resume` begins executing steps.
|
|
16
|
+
* - suspended: an action awaited `workflow.suspend`; `resume` moves back to running.
|
|
17
|
+
* - finished: all steps completed and outputs were evaluated successfully.
|
|
18
|
+
* - failed: an uncaught error bubbled out of a step or continuation.
|
|
19
|
+
*/
|
|
20
|
+
export type WorkflowRunStatus =
|
|
21
|
+
| 'idle'
|
|
22
|
+
| 'running'
|
|
23
|
+
| 'suspended'
|
|
24
|
+
| 'finished'
|
|
25
|
+
| 'failed';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lifecycle of an individual action/step captured in snapshots:
|
|
29
|
+
* - pending: defined but not yet executed (default before runner touches the step).
|
|
30
|
+
* - running: inputs evaluated and action invoked.
|
|
31
|
+
* - suspended: action paused via `workflow.suspend`; resumes to completed.
|
|
32
|
+
* - completed: action resolved (either immediately or after resume).
|
|
33
|
+
* - failed: action threw an error.
|
|
34
|
+
* - skipped: control flow bypassed the step (e.g., alternate branch or wait_any short-circuit).
|
|
35
|
+
*/
|
|
36
|
+
export type ActionStatus =
|
|
37
|
+
| 'pending'
|
|
38
|
+
| 'running'
|
|
39
|
+
| 'suspended'
|
|
40
|
+
| 'completed'
|
|
41
|
+
| 'failed'
|
|
42
|
+
| 'skipped';
|
|
43
|
+
|
|
44
|
+
export interface SuspensionOptions {
|
|
45
|
+
key?: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
data?: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ActionSnapshot {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
status: ActionStatus;
|
|
54
|
+
reason?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type StepExecutionRecord = {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
action?: string;
|
|
61
|
+
status: ActionStatus;
|
|
62
|
+
startedAt?: number;
|
|
63
|
+
endedAt?: number;
|
|
64
|
+
input?: unknown;
|
|
65
|
+
output?: unknown;
|
|
66
|
+
context?: {
|
|
67
|
+
before?: Record<string, unknown>;
|
|
68
|
+
after?: Record<string, unknown>;
|
|
69
|
+
};
|
|
70
|
+
error?: { message: string; stack?: string };
|
|
71
|
+
reason?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export interface WorkflowSnapshot {
|
|
75
|
+
status: WorkflowRunStatus;
|
|
76
|
+
actions: Record<string, ActionSnapshot>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface WorkflowRuntimeControl {
|
|
80
|
+
readonly status: WorkflowRunStatus;
|
|
81
|
+
readonly resumeData: unknown;
|
|
82
|
+
/**
|
|
83
|
+
* Suspends the currently running action and returns a promise that settles when the workflow resumes.
|
|
84
|
+
*
|
|
85
|
+
* @param options - Optional metadata describing why the suspension occurred.
|
|
86
|
+
* @returns Promise resolved with the data supplied to {@link resume}.
|
|
87
|
+
*/
|
|
88
|
+
suspend<T = unknown>(options?: SuspensionOptions): Promise<T>;
|
|
89
|
+
resume(data?: unknown): void;
|
|
90
|
+
getSnapshot(): WorkflowSnapshot;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Base context that is threaded through every workflow execution.
|
|
95
|
+
*
|
|
96
|
+
* Extend this class in user-land to expose shared services such as loggers,
|
|
97
|
+
* event emitters, database connections, or feature flags.
|
|
98
|
+
*/
|
|
99
|
+
export abstract class BaseWorkflowContext<
|
|
100
|
+
S extends Record<string, unknown> = Record<string, unknown>,
|
|
101
|
+
E = EventEmitterLike,
|
|
102
|
+
> {
|
|
103
|
+
public state: S;
|
|
104
|
+
|
|
105
|
+
private _workflowControl?: WorkflowRuntimeControl;
|
|
106
|
+
|
|
107
|
+
public abstract eventEmitter: WorkflowEventEmitterLike<E>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sets up the context and copies any initial state onto the instance.
|
|
111
|
+
*
|
|
112
|
+
* @param initialState - Optional properties to assign to the context instance.
|
|
113
|
+
* @param eventEmitter - Optional workflow event emitter to expose to actions.
|
|
114
|
+
*/
|
|
115
|
+
constructor(initialState: S) {
|
|
116
|
+
this.state = (initialState ?? {}) as S;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Snapshot the context state for telemetry or UI rendering.
|
|
121
|
+
* Override to filter or reshape the exposed state.
|
|
122
|
+
*/
|
|
123
|
+
snapshot(): Record<string, unknown> {
|
|
124
|
+
return safeCloneRecord(this.state);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Provides access to the workflow runtime API for the current execution.
|
|
129
|
+
*
|
|
130
|
+
* @returns Runtime control methods that allow suspension and inspection.
|
|
131
|
+
* @throws Error when the context is not currently attached to a runner.
|
|
132
|
+
*/
|
|
133
|
+
get workflow(): WorkflowRuntimeControl {
|
|
134
|
+
if (!this._workflowControl) {
|
|
135
|
+
throw new Error('Workflow runtime is not attached to this context.');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return this._workflowControl;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Attaches or detaches the runtime control object.
|
|
143
|
+
*
|
|
144
|
+
* @param control - Runtime control instance or `undefined` to detach.
|
|
145
|
+
* @returns Nothing; the method mutates internal state.
|
|
146
|
+
*/
|
|
147
|
+
attachWorkflowRuntime(control: WorkflowRuntimeControl | undefined): void {
|
|
148
|
+
this._workflowControl = control;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const safeCloneRecord = (
|
|
153
|
+
value: Record<string, unknown>,
|
|
154
|
+
): Record<string, unknown> => {
|
|
155
|
+
if (!value) {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof structuredClone === 'function') {
|
|
160
|
+
try {
|
|
161
|
+
return structuredClone(value) as Record<string, unknown>;
|
|
162
|
+
} catch {
|
|
163
|
+
// Fall through to JSON clone below.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
|
|
169
|
+
} catch {
|
|
170
|
+
const shallow: Record<string, unknown> = {};
|
|
171
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
172
|
+
if (typeof entry !== 'function') {
|
|
173
|
+
shallow[key] = entry;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return shallow;
|
|
178
|
+
}
|
|
179
|
+
};
|
package/src/dsl.types.ts
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import jsonata from 'jsonata';
|
|
8
|
+
import { parse as parseYaml } from 'yaml';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
import type { BindingKindSchemas } from './bindings/base-binding';
|
|
12
|
+
import { validateAndResolveBindings } from './bindings/base-binding';
|
|
13
|
+
|
|
14
|
+
export const ExpressionStringSchema = z
|
|
15
|
+
.string()
|
|
16
|
+
.regex(/^=/, 'Expression strings must start with "="')
|
|
17
|
+
.superRefine((value, ctx) => {
|
|
18
|
+
try {
|
|
19
|
+
jsonata(value.slice(1));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
const message =
|
|
22
|
+
error instanceof Error ? error.message : 'Unknown JSONata parse error';
|
|
23
|
+
ctx.addIssue({
|
|
24
|
+
code: 'custom',
|
|
25
|
+
message: `Invalid JSONata expression: ${message}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type JsonValue =
|
|
31
|
+
| string
|
|
32
|
+
| number
|
|
33
|
+
| boolean
|
|
34
|
+
| null
|
|
35
|
+
| JsonValue[]
|
|
36
|
+
| { [key: string]: JsonValue };
|
|
37
|
+
|
|
38
|
+
// Per-action timeout in milliseconds; 0 disables the timeout wrapper.
|
|
39
|
+
export const DEFAULT_TIMEOUT_MS = 0;
|
|
40
|
+
|
|
41
|
+
// Retry defaults: disabled with 3 attempts and exponential backoff starting at 25ms, capped at 10s, no jitter.
|
|
42
|
+
export const DEFAULT_RETRY_SETTINGS = {
|
|
43
|
+
enabled: false,
|
|
44
|
+
max_attempts: 3,
|
|
45
|
+
backoff_ms: 25,
|
|
46
|
+
max_delay_ms: 10_000,
|
|
47
|
+
jitter: 0,
|
|
48
|
+
multiplier: 1,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
export type InputField = {
|
|
52
|
+
type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
|
|
53
|
+
description?: string;
|
|
54
|
+
enum?: Array<string | number | boolean>;
|
|
55
|
+
items?: InputField;
|
|
56
|
+
properties?: Record<string, InputField>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type ConditionalBranch =
|
|
60
|
+
| { condition: string; steps: FlowStep[] }
|
|
61
|
+
| { else?: unknown; steps: FlowStep[] };
|
|
62
|
+
|
|
63
|
+
export type ConditionalBlock = {
|
|
64
|
+
description?: string;
|
|
65
|
+
when: ConditionalBranch[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type ParallelBlock = {
|
|
69
|
+
description?: string;
|
|
70
|
+
strategy?: 'wait_all' | 'wait_any';
|
|
71
|
+
steps: FlowStep[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type LoopAccumulate = {
|
|
75
|
+
as: string;
|
|
76
|
+
initial: JsonValue;
|
|
77
|
+
merge: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type BaseLoopStep = {
|
|
81
|
+
name?: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
accumulate?: LoopAccumulate;
|
|
84
|
+
steps: FlowStep[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type ForEachLoopStep = BaseLoopStep & {
|
|
88
|
+
type: 'for_each';
|
|
89
|
+
for_each: { item: string; in: string };
|
|
90
|
+
max_concurrency?: number;
|
|
91
|
+
until?: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type WhileLoopStep = BaseLoopStep & {
|
|
95
|
+
type: 'while';
|
|
96
|
+
while: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type LoopStep = ForEachLoopStep | WhileLoopStep;
|
|
100
|
+
|
|
101
|
+
export type FlowStep =
|
|
102
|
+
| { do: string }
|
|
103
|
+
| { parallel: ParallelBlock }
|
|
104
|
+
| { conditional: ConditionalBlock }
|
|
105
|
+
| { loop: LoopStep };
|
|
106
|
+
|
|
107
|
+
export const JsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
|
|
108
|
+
z.union([
|
|
109
|
+
z.string(),
|
|
110
|
+
z.number(),
|
|
111
|
+
z.boolean(),
|
|
112
|
+
z.null(),
|
|
113
|
+
z.array(JsonValueSchema),
|
|
114
|
+
z.record(z.string(), JsonValueSchema),
|
|
115
|
+
]),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const InputFieldSchema: z.ZodType<InputField> = z.lazy(() =>
|
|
119
|
+
z
|
|
120
|
+
.strictObject({
|
|
121
|
+
type: z.enum([
|
|
122
|
+
'string',
|
|
123
|
+
'number',
|
|
124
|
+
'integer',
|
|
125
|
+
'boolean',
|
|
126
|
+
'array',
|
|
127
|
+
'object',
|
|
128
|
+
]),
|
|
129
|
+
description: z.string().optional(),
|
|
130
|
+
enum: z.array(z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
131
|
+
items: InputFieldSchema.optional(),
|
|
132
|
+
properties: z.record(z.string(), InputFieldSchema).optional(),
|
|
133
|
+
})
|
|
134
|
+
.superRefine((value, ctx) => {
|
|
135
|
+
if (value.type === 'array' && !value.items) {
|
|
136
|
+
ctx.addIssue({
|
|
137
|
+
code: 'custom',
|
|
138
|
+
message: 'Array inputs must declare "items"',
|
|
139
|
+
path: ['items'],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (value.type !== 'array' && value.items) {
|
|
143
|
+
ctx.addIssue({
|
|
144
|
+
code: 'custom',
|
|
145
|
+
message: '"items" is only valid when type is "array"',
|
|
146
|
+
path: ['items'],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (value.type !== 'object' && value.properties) {
|
|
150
|
+
ctx.addIssue({
|
|
151
|
+
code: 'custom',
|
|
152
|
+
message: '"properties" is only valid when type is "object"',
|
|
153
|
+
path: ['properties'],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
// Retry policy: exponential backoff per attempt, capped by max_delay_ms, with optional jitter multiplier.
|
|
159
|
+
const RetriesSchema = z.strictObject({
|
|
160
|
+
enabled: z.boolean().optional().meta({
|
|
161
|
+
title: 'Enabled',
|
|
162
|
+
description:
|
|
163
|
+
'Enable retry attempts when an action fails (disabled = no retries).',
|
|
164
|
+
}),
|
|
165
|
+
max_attempts: z
|
|
166
|
+
.int()
|
|
167
|
+
.min(1)
|
|
168
|
+
.default(DEFAULT_RETRY_SETTINGS.max_attempts)
|
|
169
|
+
.meta({
|
|
170
|
+
title: 'Max Attempts',
|
|
171
|
+
description: 'Total number of attempts before giving up.',
|
|
172
|
+
}),
|
|
173
|
+
backoff_ms: z.int().min(0).default(DEFAULT_RETRY_SETTINGS.backoff_ms).meta({
|
|
174
|
+
title: 'Initial Backoff (ms)',
|
|
175
|
+
description: 'Delay in milliseconds before the first retry.',
|
|
176
|
+
}),
|
|
177
|
+
max_delay_ms: z
|
|
178
|
+
.int()
|
|
179
|
+
.min(0)
|
|
180
|
+
.default(DEFAULT_RETRY_SETTINGS.max_delay_ms)
|
|
181
|
+
.meta({
|
|
182
|
+
title: 'Max Delay (ms)',
|
|
183
|
+
description: 'Maximum delay cap in milliseconds between retries.',
|
|
184
|
+
}),
|
|
185
|
+
jitter: z.number().min(0).default(DEFAULT_RETRY_SETTINGS.jitter).meta({
|
|
186
|
+
title: 'Jitter',
|
|
187
|
+
description: 'Randomization factor applied to retry delays (0 = none).',
|
|
188
|
+
}),
|
|
189
|
+
multiplier: z
|
|
190
|
+
.number()
|
|
191
|
+
.min(1)
|
|
192
|
+
.default(DEFAULT_RETRY_SETTINGS.multiplier)
|
|
193
|
+
.meta({
|
|
194
|
+
title: 'Backoff Multiplier',
|
|
195
|
+
description: 'Multiplier applied to the delay after each retry.',
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Shared execution settings applied to every action invocation.
|
|
200
|
+
export const BaseSettingsSchema = z.strictObject({
|
|
201
|
+
timeout_ms: z.int().nonnegative().optional().meta({
|
|
202
|
+
title: 'Timeout (ms)',
|
|
203
|
+
description:
|
|
204
|
+
'Maximum runtime in milliseconds for a single action invocation (0 = disabled).',
|
|
205
|
+
}),
|
|
206
|
+
retries: RetriesSchema.optional().meta({
|
|
207
|
+
title: 'Retries',
|
|
208
|
+
description: 'Retry policy applied when an action fails.',
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
export const SettingsSchema = BaseSettingsSchema.catchall(JsonValueSchema);
|
|
213
|
+
|
|
214
|
+
const TaskBindingReferenceSchema = z.union([z.string(), z.array(z.string())]);
|
|
215
|
+
const TaskBindingsSchema = z.record(z.string(), TaskBindingReferenceSchema);
|
|
216
|
+
|
|
217
|
+
export const TASK_KIND = 'task' as const;
|
|
218
|
+
|
|
219
|
+
export const TaskDefinitionSchema = z.strictObject({
|
|
220
|
+
kind: z.literal(TASK_KIND),
|
|
221
|
+
description: z.string().optional(),
|
|
222
|
+
action: z.string(),
|
|
223
|
+
inputs: z.record(z.string(), JsonValueSchema).optional(),
|
|
224
|
+
bindings: TaskBindingsSchema.optional(),
|
|
225
|
+
settings: SettingsSchema.optional(),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const NonTaskDefinitionSchema = z.strictObject({
|
|
229
|
+
kind: z
|
|
230
|
+
.string()
|
|
231
|
+
.refine(
|
|
232
|
+
(kind) => kind !== TASK_KIND,
|
|
233
|
+
`kind "${TASK_KIND}" must follow the task definition shape.`,
|
|
234
|
+
),
|
|
235
|
+
description: z.string().optional(),
|
|
236
|
+
action: z.string().optional(),
|
|
237
|
+
settings: z.record(z.string(), z.unknown()),
|
|
238
|
+
bindings: TaskBindingsSchema.optional(),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
export const DefDefinitionSchema = z.union([
|
|
242
|
+
TaskDefinitionSchema,
|
|
243
|
+
NonTaskDefinitionSchema,
|
|
244
|
+
]);
|
|
245
|
+
const ConditionalWhenSchema: z.ZodType<ConditionalBranch> = z.lazy(() =>
|
|
246
|
+
z.strictObject({
|
|
247
|
+
condition: ExpressionStringSchema,
|
|
248
|
+
steps: z.array(FlowStepSchema),
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
const ConditionalElseSchema: z.ZodType<ConditionalBranch> = z.lazy(() =>
|
|
252
|
+
z.strictObject({
|
|
253
|
+
else: z.unknown().optional(),
|
|
254
|
+
steps: z.array(FlowStepSchema),
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
const ConditionalSchema: z.ZodType<{
|
|
258
|
+
description?: string;
|
|
259
|
+
when: ConditionalBranch[];
|
|
260
|
+
}> = z.strictObject({
|
|
261
|
+
description: z.string().optional(),
|
|
262
|
+
when: z.array(z.union([ConditionalWhenSchema, ConditionalElseSchema])).min(1),
|
|
263
|
+
});
|
|
264
|
+
const ParallelSchema: z.ZodType<ParallelBlock> = z.strictObject({
|
|
265
|
+
description: z.string().optional(),
|
|
266
|
+
strategy: z.enum(['wait_all', 'wait_any']).optional(),
|
|
267
|
+
steps: z.array(z.lazy(() => FlowStepSchema)),
|
|
268
|
+
});
|
|
269
|
+
const LoopAccumulateSchema: z.ZodType<LoopAccumulate> = z.strictObject({
|
|
270
|
+
as: z.string(),
|
|
271
|
+
initial: JsonValueSchema,
|
|
272
|
+
merge: ExpressionStringSchema,
|
|
273
|
+
});
|
|
274
|
+
const ForEachLoopSchema = z.strictObject({
|
|
275
|
+
type: z.literal('for_each'),
|
|
276
|
+
name: z.string().optional(),
|
|
277
|
+
description: z.string().optional(),
|
|
278
|
+
for_each: z.strictObject({
|
|
279
|
+
item: z.string(),
|
|
280
|
+
in: ExpressionStringSchema,
|
|
281
|
+
}),
|
|
282
|
+
max_concurrency: z.int().positive().optional(),
|
|
283
|
+
until: ExpressionStringSchema.optional(),
|
|
284
|
+
accumulate: LoopAccumulateSchema.optional(),
|
|
285
|
+
steps: z.array(z.lazy(() => FlowStepSchema)),
|
|
286
|
+
});
|
|
287
|
+
const WhileLoopSchema = z.strictObject({
|
|
288
|
+
type: z.literal('while'),
|
|
289
|
+
name: z.string().optional(),
|
|
290
|
+
description: z.string().optional(),
|
|
291
|
+
while: ExpressionStringSchema,
|
|
292
|
+
accumulate: LoopAccumulateSchema.optional(),
|
|
293
|
+
steps: z.array(z.lazy(() => FlowStepSchema)),
|
|
294
|
+
});
|
|
295
|
+
const LoopSchema: z.ZodType<LoopStep> = z.lazy(() =>
|
|
296
|
+
z.union([ForEachLoopSchema, WhileLoopSchema]),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
export const FlowStepSchema: z.ZodType<FlowStep> = z.lazy(() =>
|
|
300
|
+
z.union([
|
|
301
|
+
z.strictObject({
|
|
302
|
+
do: z.string(),
|
|
303
|
+
}),
|
|
304
|
+
z.strictObject({
|
|
305
|
+
parallel: ParallelSchema,
|
|
306
|
+
}),
|
|
307
|
+
z.strictObject({
|
|
308
|
+
conditional: ConditionalSchema,
|
|
309
|
+
}),
|
|
310
|
+
z.strictObject({
|
|
311
|
+
loop: LoopSchema,
|
|
312
|
+
}),
|
|
313
|
+
]),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
export const WorkflowMetadataSchema = z.strictObject({
|
|
317
|
+
name: z.string().min(1),
|
|
318
|
+
version: z.string().min(1),
|
|
319
|
+
description: z.string().optional(),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const InputsSchema = z.strictObject({
|
|
323
|
+
schema: z.record(z.string(), InputFieldSchema).optional(),
|
|
324
|
+
});
|
|
325
|
+
const DefaultsSchema = z.strictObject({
|
|
326
|
+
settings: SettingsSchema.optional(),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
export const WorkflowDefinitionSchema = z.strictObject({
|
|
330
|
+
inputs: InputsSchema.optional(),
|
|
331
|
+
context: z.record(z.string(), JsonValueSchema).optional(),
|
|
332
|
+
defaults: DefaultsSchema.optional(),
|
|
333
|
+
defs: z.record(z.string(), DefDefinitionSchema),
|
|
334
|
+
flow: z.array(FlowStepSchema),
|
|
335
|
+
outputs: z.record(z.string(), ExpressionStringSchema),
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
export type Settings = z.infer<typeof SettingsSchema>;
|
|
339
|
+
|
|
340
|
+
export type Conditional = z.infer<typeof ConditionalSchema>;
|
|
341
|
+
|
|
342
|
+
export type TaskDefinition = z.infer<typeof TaskDefinitionSchema>;
|
|
343
|
+
|
|
344
|
+
export type DefDefinition = z.infer<typeof DefDefinitionSchema>;
|
|
345
|
+
|
|
346
|
+
export type DefDefinitions = z.infer<typeof WorkflowDefinitionSchema>['defs'];
|
|
347
|
+
|
|
348
|
+
export type TaskDefinitions = Record<string, TaskDefinition>;
|
|
349
|
+
|
|
350
|
+
export type WorkflowMetadata = z.infer<typeof WorkflowMetadataSchema>;
|
|
351
|
+
|
|
352
|
+
export type WorkflowDefinition = z.infer<typeof WorkflowDefinitionSchema>;
|
|
353
|
+
|
|
354
|
+
export type WorkflowValidationActionMetadata = {
|
|
355
|
+
supportedBindings?: readonly string[];
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export type ValidateWorkflowOptions = {
|
|
359
|
+
bindingKinds?: BindingKindSchemas;
|
|
360
|
+
actions?: Record<string, WorkflowValidationActionMetadata>;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export type WorkflowValidationResult =
|
|
364
|
+
| { success: true; data: WorkflowDefinition }
|
|
365
|
+
| { success: false; errors: string[] };
|
|
366
|
+
|
|
367
|
+
const collectTaskReferences = (steps: FlowStep[]): string[] => {
|
|
368
|
+
const refs: string[] = [];
|
|
369
|
+
|
|
370
|
+
for (const step of steps) {
|
|
371
|
+
if ('do' in step) {
|
|
372
|
+
refs.push(step.do);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if ('parallel' in step) {
|
|
377
|
+
refs.push(...collectTaskReferences(step.parallel.steps));
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if ('conditional' in step) {
|
|
382
|
+
for (const branch of step.conditional.when) {
|
|
383
|
+
if ('condition' in branch) {
|
|
384
|
+
refs.push(...collectTaskReferences(branch.steps));
|
|
385
|
+
} else if ('else' in branch) {
|
|
386
|
+
refs.push(...collectTaskReferences(branch.steps));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if ('loop' in step) {
|
|
393
|
+
refs.push(...collectTaskReferences(step.loop.steps));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return refs;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export const isTaskDefinition = (
|
|
401
|
+
definition: DefDefinition,
|
|
402
|
+
): definition is TaskDefinition => definition.kind === TASK_KIND;
|
|
403
|
+
|
|
404
|
+
export const extractTaskDefinitions = (defs: DefDefinitions): TaskDefinitions =>
|
|
405
|
+
Object.entries(defs).reduce<TaskDefinitions>((tasks, [defName, def]) => {
|
|
406
|
+
if (isTaskDefinition(def)) {
|
|
407
|
+
tasks[defName] = def;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return tasks;
|
|
411
|
+
}, {});
|
|
412
|
+
|
|
413
|
+
const formatZodErrors = (issues: z.ZodIssue[]): string[] =>
|
|
414
|
+
issues.map((issue) => {
|
|
415
|
+
const path = issue.path.join('.') || '<root>';
|
|
416
|
+
|
|
417
|
+
return `${path}: ${issue.message}`;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
export function validateWorkflow(
|
|
421
|
+
input: string | unknown,
|
|
422
|
+
options?: ValidateWorkflowOptions,
|
|
423
|
+
): WorkflowValidationResult {
|
|
424
|
+
let candidate: unknown = input;
|
|
425
|
+
|
|
426
|
+
if (typeof input === 'string') {
|
|
427
|
+
try {
|
|
428
|
+
candidate = parseYaml(input);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
const message =
|
|
431
|
+
error instanceof Error ? error.message : 'Unknown YAML parse error';
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
success: false,
|
|
435
|
+
errors: ['Unable to parse workflow YAML', message],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const parsed = WorkflowDefinitionSchema.safeParse(candidate);
|
|
441
|
+
|
|
442
|
+
if (!parsed.success) {
|
|
443
|
+
return { success: false, errors: formatZodErrors(parsed.error.issues) };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const errors: string[] = [];
|
|
447
|
+
const taskDefinitions = extractTaskDefinitions(parsed.data.defs);
|
|
448
|
+
const referencedTasks = new Set(collectTaskReferences(parsed.data.flow));
|
|
449
|
+
const missingTasks = Array.from(referencedTasks).filter(
|
|
450
|
+
(task) => !Object.prototype.hasOwnProperty.call(taskDefinitions, task),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
if (missingTasks.length > 0) {
|
|
454
|
+
errors.push(
|
|
455
|
+
`Unknown task(s) referenced in flow: ${missingTasks.join(', ')}`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const bindingValidation = validateAndResolveBindings(parsed.data, {
|
|
460
|
+
bindingKinds: options?.bindingKinds,
|
|
461
|
+
actions: options?.actions,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (bindingValidation.errors.length > 0) {
|
|
465
|
+
errors.push(...bindingValidation.errors);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (errors.length > 0) {
|
|
469
|
+
return { success: false, errors };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return { success: true, data: parsed.data };
|
|
473
|
+
}
|