@agent-compose/sdk 0.2.3 → 0.2.4
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/README.md +145 -33
- package/dist/agent/agent-loop.d.ts +83 -5
- package/dist/agent/run-agent.d.ts +34 -9
- package/dist/client.d.ts +247 -99
- package/dist/index.d.ts +26 -11
- package/dist/index.js +1967 -745
- package/dist/processors/builtins.d.ts +35 -0
- package/dist/processors/index.d.ts +4 -0
- package/dist/processors/processor.d.ts +91 -0
- package/dist/processors/processor.test.d.ts +1 -0
- package/dist/processors/runner.d.ts +19 -0
- package/dist/request-context/index.d.ts +2 -0
- package/dist/request-context/request-context.d.ts +159 -0
- package/dist/request-context/request-context.test.d.ts +1 -0
- package/dist/runtimes/claude.d.ts +27 -50
- package/dist/runtimes/openai-desktop.js +1918 -741
- package/dist/runtimes/vercel.d.ts +34 -0
- package/dist/runtimes/vercel.js +474 -0
- package/dist/sandbox.d.ts +29 -25
- package/dist/step-invocation/__tests__/invoker.test.d.ts +1 -0
- package/dist/step-invocation/__tests__/protocol.test.d.ts +1 -0
- package/dist/step-invocation/__tests__/server.test.d.ts +1 -0
- package/dist/step-invocation/index.d.ts +25 -0
- package/dist/step-invocation/invoker.d.ts +65 -0
- package/dist/step-invocation/protocol.d.ts +44 -0
- package/dist/step-invocation/server.d.ts +63 -0
- package/dist/step-invocation/types.d.ts +72 -0
- package/dist/tools/coding.d.ts +49 -0
- package/dist/tools/coding.test.d.ts +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/types/events.d.ts +36 -0
- package/dist/types/execution-context.d.ts +22 -0
- package/dist/types/runtime.d.ts +32 -0
- package/dist/types/sandbox-environment.d.ts +5 -2
- package/dist/types/sandbox.d.ts +14 -12
- package/dist/types/workflow-metadata.d.ts +51 -0
- package/dist/types/workflow-plan.d.ts +19 -0
- package/dist/types/workflow.d.ts +57 -17
- package/dist/utils/bundler.d.ts +62 -3
- package/dist/workflow-steps/__tests__/observability.test.d.ts +1 -0
- package/dist/workflow-steps/index.d.ts +10 -0
- package/dist/workflow-steps/observability.d.ts +58 -0
- package/dist/workflow-steps/runner.d.ts +96 -0
- package/dist/workflow-steps/step.d.ts +25 -0
- package/dist/workflow-steps/types.d.ts +135 -0
- package/dist/workflow-steps/workflow-steps.test.d.ts +1 -0
- package/dist/workflow-steps/workflow.d.ts +50 -0
- package/dist/workflows/engine.d.ts +27 -13
- package/dist/workflows/invoke-child.d.ts +10 -0
- package/package.json +25 -15
- package/src/agent/agent-loop.ts +197 -26
- package/src/agent/run-agent.ts +40 -15
- package/src/client.ts +326 -76
- package/src/index.ts +124 -10
- package/src/processors/builtins.ts +72 -0
- package/src/processors/index.ts +15 -0
- package/src/processors/processor.ts +103 -0
- package/src/processors/runner.ts +42 -0
- package/src/request-context/index.ts +17 -0
- package/src/request-context/request-context.ts +302 -0
- package/src/runtimes/claude.ts +123 -254
- package/src/runtimes/vercel.ts +180 -0
- package/src/sandbox.ts +53 -21
- package/src/step-invocation/index.ts +33 -0
- package/src/step-invocation/invoker.ts +204 -0
- package/src/step-invocation/protocol.ts +57 -0
- package/src/step-invocation/server.ts +184 -0
- package/src/step-invocation/types.ts +70 -0
- package/src/tools/coding.ts +126 -0
- package/src/tools/index.ts +8 -0
- package/src/types/events.ts +40 -0
- package/src/types/execution-context.ts +30 -0
- package/src/types/runtime.ts +24 -0
- package/src/types/sandbox-environment.ts +7 -5
- package/src/types/sandbox.ts +16 -12
- package/src/types/workflow-metadata.ts +84 -0
- package/src/types/workflow-plan.ts +24 -0
- package/src/types/workflow.ts +139 -25
- package/src/utils/bundler.ts +198 -18
- package/src/utils/source-loader.ts +2 -2
- package/src/workflow-steps/index.ts +30 -0
- package/src/workflow-steps/observability.ts +103 -0
- package/src/workflow-steps/runner.ts +244 -0
- package/src/workflow-steps/step.ts +38 -0
- package/src/workflow-steps/types.ts +134 -0
- package/src/workflow-steps/workflow.ts +95 -0
- package/src/workflows/engine.ts +69 -40
- package/src/workflows/invoke-child.ts +29 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runWorkflowSteps` — in-process step executor for a `Workflow`.
|
|
3
|
+
*
|
|
4
|
+
* Walks the step chain sequentially:
|
|
5
|
+
* 1. Validate workflow input against `workflow.input`.
|
|
6
|
+
* 2. For each step:
|
|
7
|
+
* a. Optionally check `getCachedOutput(stepIndex, step.name)` — if a
|
|
8
|
+
* previous run completed this step, skip and reuse its output.
|
|
9
|
+
* (Phase 1b uses this for crash recovery.)
|
|
10
|
+
* b. Validate current input against `step.input`.
|
|
11
|
+
* c. Call `step.run({ input, ... })`.
|
|
12
|
+
* d. Validate return value against `step.output`.
|
|
13
|
+
* e. Call `onStepCompleted(stepIndex, step.name, output, durationMs)`.
|
|
14
|
+
* f. Output becomes the next step's input.
|
|
15
|
+
* 3. Validate final output against `workflow.output`.
|
|
16
|
+
*
|
|
17
|
+
* Errors during any step bubble through `onStepFailed` and re-throw so the
|
|
18
|
+
* caller can decide whether to mark the run failed.
|
|
19
|
+
*
|
|
20
|
+
* The cache + completion hooks are injection points — a sandbox engine
|
|
21
|
+
* adapter (Phase 1c) wires them to `workflow_step_runs` rows. The default
|
|
22
|
+
* is an in-process map for tests.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import type { Workflow, StepContext, StepRunResult } from "./types.js";
|
|
27
|
+
import type { RequestContext } from "../request-context/request-context.js";
|
|
28
|
+
import type { SandboxProvider } from "../types/sandbox.js";
|
|
29
|
+
import type { WorkflowRun, WorkflowCtx } from "../types/workflow.js";
|
|
30
|
+
import { StepObservabilityCollector, type StepObservability } from "./observability.js";
|
|
31
|
+
|
|
32
|
+
export class StepValidationError extends Error {
|
|
33
|
+
readonly kind = "step-validation" as const;
|
|
34
|
+
constructor(
|
|
35
|
+
readonly stepName: string,
|
|
36
|
+
readonly side: "input" | "output",
|
|
37
|
+
cause: z.ZodError,
|
|
38
|
+
) {
|
|
39
|
+
super(`Step "${stepName}" ${side} failed schema validation: ${cause.message}`);
|
|
40
|
+
this.name = "StepValidationError";
|
|
41
|
+
this.cause = cause;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class WorkflowInputValidationError extends Error {
|
|
46
|
+
readonly kind = "workflow-input-validation" as const;
|
|
47
|
+
constructor(readonly workflowId: string, cause: z.ZodError) {
|
|
48
|
+
super(`Workflow "${workflowId}" input failed schema validation: ${cause.message}`);
|
|
49
|
+
this.name = "WorkflowInputValidationError";
|
|
50
|
+
this.cause = cause;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class WorkflowOutputValidationError extends Error {
|
|
55
|
+
readonly kind = "workflow-output-validation" as const;
|
|
56
|
+
constructor(readonly workflowId: string, cause: z.ZodError) {
|
|
57
|
+
super(`Workflow "${workflowId}" output failed schema validation: ${cause.message}`);
|
|
58
|
+
this.name = "WorkflowOutputValidationError";
|
|
59
|
+
this.cause = cause;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RunWorkflowStepsOpts<TInput, TOutput> {
|
|
64
|
+
workflow: Workflow<TInput, TOutput>;
|
|
65
|
+
input: TInput;
|
|
66
|
+
run: WorkflowRun;
|
|
67
|
+
requestContext: RequestContext;
|
|
68
|
+
sandbox?: SandboxProvider;
|
|
69
|
+
abortSignal?: AbortSignal;
|
|
70
|
+
/**
|
|
71
|
+
* Crash-recovery hook. Called before a step executes. Return the cached
|
|
72
|
+
* output to skip execution; return undefined to run the step.
|
|
73
|
+
*
|
|
74
|
+
* Phase 1b implementations will look up `workflow_step_runs` rows for
|
|
75
|
+
* (runId, stepIndex, stepName) and return completed step outputs here.
|
|
76
|
+
* Default: always undefined (no caching).
|
|
77
|
+
*/
|
|
78
|
+
getCachedOutput?(stepIndex: number, stepName: string): unknown | undefined | Promise<unknown | undefined>;
|
|
79
|
+
/** Fire after a step's `execute` and output validation succeed. */
|
|
80
|
+
onStepCompleted?(stepIndex: number, stepName: string, output: unknown, durationMs: number): void | Promise<void>;
|
|
81
|
+
/** Fire when a step throws or fails validation. The error is re-thrown after this returns. */
|
|
82
|
+
onStepFailed?(stepIndex: number, stepName: string, error: Error, durationMs: number): void | Promise<void>;
|
|
83
|
+
/** Fire before a step runs (after cache check / before input validation). */
|
|
84
|
+
onStepStarted?(stepIndex: number, stepName: string): void | Promise<void>;
|
|
85
|
+
/** Child workflow invocation implementation. Defaults to a clear unsupported error. */
|
|
86
|
+
invokeChild?: WorkflowCtx["invokeChild"];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RunWorkflowStepsResult<TOutput> {
|
|
90
|
+
output: TOutput;
|
|
91
|
+
/** Per-step results in order — useful for tests and lifecycle event emission. */
|
|
92
|
+
steps: ReadonlyArray<{ name: string } & StepRunResult>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface RunWorkflowSingleStepOpts {
|
|
96
|
+
workflow: Workflow<unknown, unknown>;
|
|
97
|
+
stepIndex: number;
|
|
98
|
+
input: unknown;
|
|
99
|
+
run: WorkflowRun;
|
|
100
|
+
requestContext: RequestContext;
|
|
101
|
+
sandbox?: SandboxProvider;
|
|
102
|
+
abortSignal?: AbortSignal;
|
|
103
|
+
invokeChild?: WorkflowCtx["invokeChild"];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Result of one step run — output plus whatever the step's observability
|
|
107
|
+
* hooks recorded. `observability` is undefined when nothing was buffered,
|
|
108
|
+
* so callers can drop it from the wire payload entirely. */
|
|
109
|
+
export interface RunWorkflowSingleStepResult {
|
|
110
|
+
output: unknown;
|
|
111
|
+
observability?: StepObservability;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function runWorkflowSingleStep(opts: RunWorkflowSingleStepOpts): Promise<RunWorkflowSingleStepResult> {
|
|
115
|
+
const step = opts.workflow.steps[opts.stepIndex];
|
|
116
|
+
if (!step) throw new Error(`Step index ${opts.stepIndex} not found in workflow "${opts.workflow.id}"`);
|
|
117
|
+
const parsedInput = step.input.safeParse(opts.input);
|
|
118
|
+
if (!parsedInput.success) throw new StepValidationError(step.name, "input", parsedInput.error);
|
|
119
|
+
const collector = new StepObservabilityCollector();
|
|
120
|
+
const stepCtx: StepContext<unknown> = {
|
|
121
|
+
input: parsedInput.data,
|
|
122
|
+
requestContext: opts.requestContext,
|
|
123
|
+
run: opts.run,
|
|
124
|
+
...(opts.sandbox ? { sandbox: opts.sandbox } : {}),
|
|
125
|
+
stepName: step.name,
|
|
126
|
+
abortSignal: opts.abortSignal ?? new AbortController().signal,
|
|
127
|
+
invokeChild: opts.invokeChild ?? (() => { throw new Error("StepContext.invokeChild is not configured for this workflow engine"); }),
|
|
128
|
+
setMetadata: collector.setMetadata,
|
|
129
|
+
step: collector.step,
|
|
130
|
+
agentEvents: collector.agentEvents,
|
|
131
|
+
};
|
|
132
|
+
const output = await step.run(stepCtx);
|
|
133
|
+
const parsedOutput = step.output.safeParse(output);
|
|
134
|
+
if (!parsedOutput.success) throw new StepValidationError(step.name, "output", parsedOutput.error);
|
|
135
|
+
const observability = collector.snapshot();
|
|
136
|
+
return observability === undefined
|
|
137
|
+
? { output: parsedOutput.data }
|
|
138
|
+
: { output: parsedOutput.data, observability };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function runWorkflowSteps<TInput, TOutput>(
|
|
142
|
+
opts: RunWorkflowStepsOpts<TInput, TOutput>,
|
|
143
|
+
): Promise<RunWorkflowStepsResult<TOutput>> {
|
|
144
|
+
const { workflow, run, requestContext } = opts;
|
|
145
|
+
const abortSignal = opts.abortSignal ?? new AbortController().signal;
|
|
146
|
+
const invokeChild: WorkflowCtx["invokeChild"] = opts.invokeChild ?? (() => {
|
|
147
|
+
throw new Error("StepContext.invokeChild is not configured for this workflow engine");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Validate workflow input up front. If the caller supplied junk, none of
|
|
151
|
+
// the steps run — cleaner than letting step 1 fail with its own message.
|
|
152
|
+
const parsedInput = workflow.input.safeParse(opts.input);
|
|
153
|
+
if (!parsedInput.success) {
|
|
154
|
+
throw new WorkflowInputValidationError(workflow.id, parsedInput.error);
|
|
155
|
+
}
|
|
156
|
+
let current: unknown = parsedInput.data;
|
|
157
|
+
|
|
158
|
+
const stepResults: Array<{ name: string } & StepRunResult> = [];
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < workflow.steps.length; i++) {
|
|
161
|
+
const step = workflow.steps[i];
|
|
162
|
+
const cached = opts.getCachedOutput ? await opts.getCachedOutput(i, step.name) : undefined;
|
|
163
|
+
if (cached !== undefined) {
|
|
164
|
+
// Re-validate cached output so a corrupted row can't poison the chain.
|
|
165
|
+
const parsedCached = step.output.safeParse(cached);
|
|
166
|
+
if (!parsedCached.success) {
|
|
167
|
+
throw new StepValidationError(step.name, "output", parsedCached.error);
|
|
168
|
+
}
|
|
169
|
+
current = parsedCached.data;
|
|
170
|
+
stepResults.push({ name: step.name, status: "completed", output: parsedCached.data, durationMs: 0 });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await opts.onStepStarted?.(i, step.name);
|
|
175
|
+
const parsedStepInput = step.input.safeParse(current);
|
|
176
|
+
if (!parsedStepInput.success) {
|
|
177
|
+
const err = new StepValidationError(step.name, "input", parsedStepInput.error);
|
|
178
|
+
await opts.onStepFailed?.(i, step.name, err, 0);
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const collector = new StepObservabilityCollector();
|
|
183
|
+
const stepCtx: StepContext<unknown> = {
|
|
184
|
+
input: parsedStepInput.data,
|
|
185
|
+
requestContext,
|
|
186
|
+
run,
|
|
187
|
+
...(opts.sandbox ? { sandbox: opts.sandbox } : {}),
|
|
188
|
+
stepName: step.name,
|
|
189
|
+
abortSignal,
|
|
190
|
+
invokeChild,
|
|
191
|
+
setMetadata: collector.setMetadata,
|
|
192
|
+
step: collector.step,
|
|
193
|
+
agentEvents: collector.agentEvents,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const startedAt = Date.now();
|
|
197
|
+
let output: unknown;
|
|
198
|
+
try {
|
|
199
|
+
output = await step.run(stepCtx);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
202
|
+
const durationMs = Date.now() - startedAt;
|
|
203
|
+
const observability = collector.snapshot();
|
|
204
|
+
stepResults.push({
|
|
205
|
+
name: step.name, status: "failed", error: wrapped.message, durationMs,
|
|
206
|
+
...(observability ? { observability } : {}),
|
|
207
|
+
});
|
|
208
|
+
await opts.onStepFailed?.(i, step.name, wrapped, durationMs);
|
|
209
|
+
throw wrapped;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const parsedOutput = step.output.safeParse(output);
|
|
213
|
+
if (!parsedOutput.success) {
|
|
214
|
+
const err = new StepValidationError(step.name, "output", parsedOutput.error);
|
|
215
|
+
const durationMs = Date.now() - startedAt;
|
|
216
|
+
const observability = collector.snapshot();
|
|
217
|
+
stepResults.push({
|
|
218
|
+
name: step.name, status: "failed", error: err.message, durationMs,
|
|
219
|
+
...(observability ? { observability } : {}),
|
|
220
|
+
});
|
|
221
|
+
await opts.onStepFailed?.(i, step.name, err, durationMs);
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const durationMs = Date.now() - startedAt;
|
|
226
|
+
current = parsedOutput.data;
|
|
227
|
+
const observability = collector.snapshot();
|
|
228
|
+
stepResults.push({
|
|
229
|
+
name: step.name, status: "completed", output: parsedOutput.data, durationMs,
|
|
230
|
+
...(observability ? { observability } : {}),
|
|
231
|
+
});
|
|
232
|
+
await opts.onStepCompleted?.(i, step.name, parsedOutput.data, durationMs);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Validate workflow output against the declared `output` schema. The
|
|
236
|
+
// chain already ensured the final step's `output` matched at construction
|
|
237
|
+
// time, but parsing again here defends against corrupted cached outputs
|
|
238
|
+
// that bypassed step validation.
|
|
239
|
+
const parsedFinal = workflow.output.safeParse(current);
|
|
240
|
+
if (!parsedFinal.success) {
|
|
241
|
+
throw new WorkflowOutputValidationError(workflow.id, parsedFinal.error);
|
|
242
|
+
}
|
|
243
|
+
return { output: parsedFinal.data as TOutput, steps: stepResults };
|
|
244
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineStep` — typed factory for one workflow step.
|
|
3
|
+
*
|
|
4
|
+
* Required:
|
|
5
|
+
* - `name` stable identifier (also the step row key)
|
|
6
|
+
* - `input` Zod schema; validated before `run` executes
|
|
7
|
+
* - `output` Zod schema; validated against `run`'s return value
|
|
8
|
+
* - `run` step body
|
|
9
|
+
*
|
|
10
|
+
* Validation is required (not optional) because the durability story rides
|
|
11
|
+
* on every step boundary being a recordable, replayable JSON value. A step
|
|
12
|
+
* without a schema is invisible to the engine's persistence layer.
|
|
13
|
+
*
|
|
14
|
+
* Type inference flows: `defineStep` infers TInput/TOutput from the schemas
|
|
15
|
+
* so `run(ctx)` is fully typed via `ctx.input` at the call site.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { z } from "zod";
|
|
19
|
+
import type { Step, StepContext } from "./types.js";
|
|
20
|
+
|
|
21
|
+
export interface DefineStepOpts<TInput, TOutput> {
|
|
22
|
+
name: string;
|
|
23
|
+
input: z.ZodType<TInput>;
|
|
24
|
+
output: z.ZodType<TOutput>;
|
|
25
|
+
run(ctx: StepContext<TInput>): TOutput | Promise<TOutput>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function defineStep<TInput, TOutput>(
|
|
29
|
+
opts: DefineStepOpts<TInput, TOutput>,
|
|
30
|
+
): Step<TInput, TOutput> {
|
|
31
|
+
if (!opts.name) throw new Error("defineStep: 'name' is required");
|
|
32
|
+
return {
|
|
33
|
+
name: opts.name,
|
|
34
|
+
input: opts.input,
|
|
35
|
+
output: opts.output,
|
|
36
|
+
run: opts.run,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the declarative workflow shape.
|
|
3
|
+
*
|
|
4
|
+
* Workflows-as-data — every workflow is a list of typed steps with explicit
|
|
5
|
+
* input/output Zod schemas. Mastra's shape, adapted for our runtime.
|
|
6
|
+
*
|
|
7
|
+
* Why: durable suspend/resume requires step boundaries to be addressable as
|
|
8
|
+
* data, not opaque async-function bodies. Each step's input + output is
|
|
9
|
+
* serialisable JSON so engine adapters (in-process, sandbox, future
|
|
10
|
+
* Inngest/Temporal) can record completion and replay from the last
|
|
11
|
+
* completed step.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { z } from "zod";
|
|
15
|
+
import type { BaseExecutionContext } from "../types/execution-context.js";
|
|
16
|
+
import type { AgentEventSink } from "../types/workflow.js";
|
|
17
|
+
import type { WorkflowMetadata } from "../types/workflow-metadata.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Per-step execution context. Threaded into every step's `execute(...)` so
|
|
21
|
+
* the step can read tenant identity and run identity, log progress, and
|
|
22
|
+
* invoke sandbox commands.
|
|
23
|
+
*
|
|
24
|
+
* Sandbox is optional because in-process tests run steps without a sandbox.
|
|
25
|
+
*/
|
|
26
|
+
export interface StepContext<TInput = unknown> extends BaseExecutionContext {
|
|
27
|
+
/** Validated step input — already parsed against the step's `input` schema. */
|
|
28
|
+
input: TInput;
|
|
29
|
+
/** Step name — useful for logging. */
|
|
30
|
+
stepName: string;
|
|
31
|
+
/** AbortSignal that fires on workflow cancellation. */
|
|
32
|
+
abortSignal: AbortSignal;
|
|
33
|
+
/**
|
|
34
|
+
* Merge key-value metadata onto the run record. Buffered during the
|
|
35
|
+
* step and flushed when the step completes; the durable engine writes
|
|
36
|
+
* it via the same `mergeRunMetadata` path the legacy runner used, so
|
|
37
|
+
* the dashboard sees the same shape. Later keys win.
|
|
38
|
+
*/
|
|
39
|
+
setMetadata(data: Record<string, unknown>): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Wrap a named sub-step for observability. Emits
|
|
42
|
+
* `workflow_substep_started` / `workflow_substep_completed` /
|
|
43
|
+
* `workflow_substep_failed` lifecycle events on the run timeline with
|
|
44
|
+
* the measured `durationMs`. Use for long sub-phases inside one step
|
|
45
|
+
* (setup, external API call, submit).
|
|
46
|
+
*
|
|
47
|
+
* The block runs even if observability flushing fails — instrumentation
|
|
48
|
+
* never breaks the workflow.
|
|
49
|
+
*/
|
|
50
|
+
step<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Sink for `agent({ events: ctx.agentEvents })`. Each emitted
|
|
53
|
+
* `AgentLifecycleEvent` is buffered and flushed at step end, then
|
|
54
|
+
* fanned out to the run's SSE stream. Mid-step the events are not
|
|
55
|
+
* yet visible to consumers — that's a deliberate tradeoff for the
|
|
56
|
+
* step-mode, /internal/*-free runner.
|
|
57
|
+
*/
|
|
58
|
+
agentEvents: AgentEventSink;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Step definition — a single typed unit of work in a workflow chain.
|
|
63
|
+
*
|
|
64
|
+
* Both `input` and `output` are required: the workflow shape's value
|
|
65
|
+
* comes from every step boundary being recordable. Optional schemas
|
|
66
|
+
* would defeat the durability story.
|
|
67
|
+
*
|
|
68
|
+
* `run` may be sync or async; the engine awaits it uniformly.
|
|
69
|
+
*/
|
|
70
|
+
export interface Step<TInput, TOutput> {
|
|
71
|
+
/** Stable identifier — used as the step row key and for logs/metrics. */
|
|
72
|
+
readonly name: string;
|
|
73
|
+
/** Zod schema validated against the step's input before `run`. */
|
|
74
|
+
readonly input: z.ZodType<TInput>;
|
|
75
|
+
/** Zod schema validated against `run`'s return value. */
|
|
76
|
+
readonly output: z.ZodType<TOutput>;
|
|
77
|
+
/** Step body. Receives a `StepContext<TInput>` and returns the typed output. */
|
|
78
|
+
run(ctx: StepContext<TInput>): TOutput | Promise<TOutput>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The result of running one step. Engine adapters persist these into the
|
|
83
|
+
* `workflow_step_runs` table (Phase 1b) so subsequent runs can skip
|
|
84
|
+
* completed steps. `observability` carries the snapshot of
|
|
85
|
+
* `ctx.setMetadata` / `ctx.step` / `ctx.agentEvents` recorded during
|
|
86
|
+
* the step; undefined when no hooks were used.
|
|
87
|
+
*/
|
|
88
|
+
export type StepRunResult<TOutput = unknown> =
|
|
89
|
+
| { status: "completed"; output: TOutput; durationMs: number; observability?: import("./observability.js").StepObservability }
|
|
90
|
+
| { status: "failed"; error: string; durationMs: number; observability?: import("./observability.js").StepObservability };
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Workflow — a list of typed steps plus the workflow's input/output
|
|
94
|
+
* schemas plus its server-side metadata bag. Returned by `defineWorkflow(...)`
|
|
95
|
+
* (run form) and `defineWorkflow(...).step(...)...build()` (step form).
|
|
96
|
+
* Engine adapters consume this shape.
|
|
97
|
+
*
|
|
98
|
+
* `input` validates the workflow input before the first step runs.
|
|
99
|
+
* `output` validates the final step's output before the workflow
|
|
100
|
+
* completes successfully.
|
|
101
|
+
*
|
|
102
|
+
* `metadata` carries server-readable declarations — network policy,
|
|
103
|
+
* brokered-secret placeholders, snapshot reference/capture defaults,
|
|
104
|
+
* workflow-level processor chain. The bundler reads these at registration
|
|
105
|
+
* time. Always present, possibly empty.
|
|
106
|
+
*
|
|
107
|
+
* Historically named `CompiledWorkflow` back when an uncompiled form
|
|
108
|
+
* existed (a bare `WorkflowFn` was its own engine input). After the
|
|
109
|
+
* legacy delete in #55 every workflow goes through this shape, so the
|
|
110
|
+
* "Compiled" prefix carried no information — it's just `Workflow` now.
|
|
111
|
+
*/
|
|
112
|
+
export interface Workflow<TInput, TOutput> {
|
|
113
|
+
/** Stable workflow identifier. */
|
|
114
|
+
readonly id: string;
|
|
115
|
+
/** Validates the initial workflow input. */
|
|
116
|
+
readonly input: z.ZodType<TInput>;
|
|
117
|
+
/** Validates the final step's output. */
|
|
118
|
+
readonly output: z.ZodType<TOutput>;
|
|
119
|
+
/** Sequential list of steps to execute. Type chain proven at compile time. */
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
121
|
+
readonly steps: ReadonlyArray<Step<any, any>>;
|
|
122
|
+
/** Server-readable declarations: network policy, placeholders, snapshot
|
|
123
|
+
* defaults, processors. Always set; empty object when nothing declared. */
|
|
124
|
+
readonly metadata: WorkflowMetadata;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Discriminator brand stamped on every `Workflow` so `isWorkflow` can
|
|
129
|
+
* distinguish a real workflow from an arbitrary default export at
|
|
130
|
+
* registration / dispatch time. Workflow authors never see this —
|
|
131
|
+
* `defineWorkflow` and `defineWorkflow(...).step(...).build()` set it
|
|
132
|
+
* internally.
|
|
133
|
+
*/
|
|
134
|
+
export const WORKFLOW_BRAND = Symbol.for("@agent-compose/sdk.Workflow");
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createStepWorkflow` — internal typed builder for a step-based workflow.
|
|
3
|
+
*
|
|
4
|
+
* const wf = defineWorkflow({
|
|
5
|
+
* id: "my-workflow",
|
|
6
|
+
* input: z.object({ url: z.string().url() }),
|
|
7
|
+
* output: z.object({ pageTitle: z.string() }),
|
|
8
|
+
* })
|
|
9
|
+
* .step(fetchStep)
|
|
10
|
+
* .step(parseStep)
|
|
11
|
+
* .build();
|
|
12
|
+
*
|
|
13
|
+
* The chain enforces that each step's input matches the previous step's
|
|
14
|
+
* output at compile time. `.build()` returns a `Workflow` and verifies
|
|
15
|
+
* at construction time that the workflow's declared `output` schema
|
|
16
|
+
* matches the final step's `output` schema.
|
|
17
|
+
*
|
|
18
|
+
* Workflows-as-data — the result is consumable by any engine adapter
|
|
19
|
+
* (in-process today; sandbox + Inngest/Temporal in future PRs).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { z } from "zod";
|
|
23
|
+
import type { Step, Workflow } from "./types.js";
|
|
24
|
+
import { WORKFLOW_BRAND } from "./types.js";
|
|
25
|
+
import { extractMetadata } from "../types/workflow-metadata.js";
|
|
26
|
+
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
27
|
+
import type { Processor } from "../processors/processor.js";
|
|
28
|
+
|
|
29
|
+
export interface WorkflowBuilder<TInput, TCurrent> {
|
|
30
|
+
/** Append a step whose input matches the current builder output. */
|
|
31
|
+
step<TOutput>(step: Step<TCurrent, TOutput>): WorkflowBuilder<TInput, TOutput>;
|
|
32
|
+
/**
|
|
33
|
+
* Freeze the chain into a `Workflow`. Throws when no steps have been
|
|
34
|
+
* added or when the final step's `output` schema is not the same Zod
|
|
35
|
+
* schema instance as the workflow's declared `output` schema.
|
|
36
|
+
*
|
|
37
|
+
* The schema-identity check catches the "I edited one of two schemas"
|
|
38
|
+
* footgun that's easy to introduce when output types drift.
|
|
39
|
+
*/
|
|
40
|
+
build(): Workflow<TInput, TCurrent>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface StepWorkflowDefinition<TInput, TOutput> {
|
|
44
|
+
id: string;
|
|
45
|
+
input: z.ZodType<TInput>;
|
|
46
|
+
output: z.ZodType<TOutput>;
|
|
47
|
+
networkPolicy?: SandboxNetworkPolicy;
|
|
48
|
+
placeholders?: Record<string, string>;
|
|
49
|
+
snapshot?: string;
|
|
50
|
+
saveSnapshot?: boolean;
|
|
51
|
+
processors?: readonly Processor[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createStepWorkflow<TInput, TOutput>(
|
|
55
|
+
opts: StepWorkflowDefinition<TInput, TOutput>,
|
|
56
|
+
): WorkflowBuilder<TInput, TInput> {
|
|
57
|
+
if (!opts.id) throw new Error("defineWorkflow: 'id' is required for step-based workflows");
|
|
58
|
+
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
const build = <TCurrent,>(steps: ReadonlyArray<Step<any, any>>): WorkflowBuilder<TInput, TCurrent> => ({
|
|
61
|
+
step<TNext>(step: Step<TCurrent, TNext>): WorkflowBuilder<TInput, TNext> {
|
|
62
|
+
return build<TNext>([...steps, step]);
|
|
63
|
+
},
|
|
64
|
+
build(): Workflow<TInput, TCurrent> {
|
|
65
|
+
if (steps.length === 0) {
|
|
66
|
+
throw new Error(`defineWorkflow("${opts.id}").build: no steps registered — add at least one with .step(step)`);
|
|
67
|
+
}
|
|
68
|
+
const finalStep = steps[steps.length - 1];
|
|
69
|
+
if (finalStep.output !== opts.output as unknown) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`defineWorkflow("${opts.id}").build: final step "${finalStep.name}" output schema is not the same Zod schema instance as the workflow's declared output schema. ` +
|
|
72
|
+
`Pass the same schema reference to both the final step and defineWorkflow.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const workflow: Workflow<TInput, TCurrent> = {
|
|
76
|
+
id: opts.id,
|
|
77
|
+
input: opts.input,
|
|
78
|
+
output: opts.output as unknown as z.ZodType<TCurrent>,
|
|
79
|
+
steps: Object.freeze([...steps]),
|
|
80
|
+
metadata: extractMetadata(opts),
|
|
81
|
+
};
|
|
82
|
+
// Brand BEFORE freezing — defineProperty is the only way to set a
|
|
83
|
+
// non-enumerable key, and frozen objects refuse it.
|
|
84
|
+
Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
|
|
85
|
+
return Object.freeze(workflow);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return build<TInput>([]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Type guard — true when `value` is a `Workflow`. */
|
|
93
|
+
export function isWorkflow(value: unknown): value is Workflow<unknown, unknown> {
|
|
94
|
+
return typeof value === "object" && value !== null && (value as Record<symbol, unknown>)[WORKFLOW_BRAND] === true;
|
|
95
|
+
}
|
package/src/workflows/engine.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow engine —
|
|
2
|
+
* Workflow engine — drives a `Workflow`'s steps.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `AgentComposeClient.invoke[AndWait](...)`. The engine's only job now is:
|
|
4
|
+
* Single execution entry point: `runWorkflow`. Every workflow has the same
|
|
5
|
+
* shape (defineWorkflow always returns a `Workflow`), so there's one
|
|
6
|
+
* runner.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - classify errors into `WorkflowError` vs `EngineError` for the runner
|
|
12
|
-
* harness to emit on /fail
|
|
8
|
+
* Errors classified into `WorkflowError` (user code threw) vs `EngineError`
|
|
9
|
+
* (platform problem) for the runner harness to surface upstream.
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
|
-
import type { WorkflowHooks, WorkflowCtx
|
|
12
|
+
import type { WorkflowHooks, WorkflowCtx } from "../types/workflow.js";
|
|
16
13
|
import { makeLocalSandboxProvider } from "../sandbox.js";
|
|
17
14
|
import { formatError } from "../utils/errors.js";
|
|
15
|
+
import { RequestContext } from "../request-context/request-context.js";
|
|
16
|
+
import type { Workflow } from "../workflow-steps/types.js";
|
|
17
|
+
import {
|
|
18
|
+
runWorkflowSteps,
|
|
19
|
+
type RunWorkflowStepsOpts,
|
|
20
|
+
} from "../workflow-steps/runner.js";
|
|
18
21
|
|
|
19
22
|
// ── Error classification ─────────────────────────────────────────────────────
|
|
20
23
|
|
|
@@ -70,41 +73,67 @@ export interface WorkflowResult<T = unknown> {
|
|
|
70
73
|
response: T | null;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
function defaultRequestContext(runId: string, workflowId = ""): RequestContext {
|
|
77
|
+
return RequestContext.fromReserved({
|
|
78
|
+
teamId: "",
|
|
79
|
+
runId,
|
|
80
|
+
workflowId,
|
|
81
|
+
factoryId: null,
|
|
82
|
+
apiKeyScopes: [],
|
|
83
|
+
parentRunId: null,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RunWorkflowOptions {
|
|
88
|
+
hooks?: WorkflowHooks;
|
|
89
|
+
requestContext?: RequestContext;
|
|
90
|
+
getCachedOutput?: RunWorkflowStepsOpts<unknown, unknown>["getCachedOutput"];
|
|
91
|
+
onStepStarted?: RunWorkflowStepsOpts<unknown, unknown>["onStepStarted"];
|
|
92
|
+
onStepCompleted?: RunWorkflowStepsOpts<unknown, unknown>["onStepCompleted"];
|
|
93
|
+
onStepFailed?: RunWorkflowStepsOpts<unknown, unknown>["onStepFailed"];
|
|
94
|
+
/** Provider-specific child workflow invocation. Temporal/Inngest providers
|
|
95
|
+
* inject their native child-workflow primitive; the LocalProvider injects
|
|
96
|
+
* the public Agent Compose API client. */
|
|
97
|
+
invokeChild?: WorkflowCtx["invokeChild"];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function runWorkflow<TInput, TOutput>(
|
|
101
|
+
wf: Workflow<TInput, TOutput>,
|
|
102
|
+
ctx: { run: { id: string }; input?: TInput },
|
|
103
|
+
opts?: RunWorkflowOptions,
|
|
104
|
+
): Promise<WorkflowResult<TOutput>> {
|
|
78
105
|
const hooks = opts?.hooks ?? {};
|
|
79
|
-
const
|
|
80
|
-
// Single SandboxProvider for the whole run — handed to the workflow as
|
|
81
|
-
// its second positional arg. Stateless wrapper over child_process.spawn
|
|
82
|
-
// + fs.writeFile, so reuse is purely a matter of shape consistency.
|
|
106
|
+
const requestContext = opts?.requestContext ?? defaultRequestContext(ctx.run.id, wf.id);
|
|
83
107
|
const sandbox = makeLocalSandboxProvider();
|
|
108
|
+
const invokeChild = opts?.invokeChild ?? (() => {
|
|
109
|
+
throw new Error("StepContext.invokeChild is not configured for this workflow engine");
|
|
110
|
+
});
|
|
84
111
|
|
|
85
|
-
let workflowErr: unknown;
|
|
86
|
-
let wfOutput: unknown;
|
|
87
112
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const startedAt = Date.now();
|
|
94
|
-
hooks.onStepStart?.(name);
|
|
95
|
-
try { const r = await fn(); hooks.onStepComplete?.(name, Date.now() - startedAt); return r; }
|
|
96
|
-
catch (err) { hooks.onStepFail?.(name, Date.now() - startedAt, formatError(err)); throw err; }
|
|
97
|
-
},
|
|
98
|
-
},
|
|
113
|
+
const result = await runWorkflowSteps<TInput, TOutput>({
|
|
114
|
+
workflow: wf,
|
|
115
|
+
input: (ctx.input as TInput),
|
|
116
|
+
run: ctx.run,
|
|
117
|
+
requestContext,
|
|
99
118
|
sandbox,
|
|
100
|
-
|
|
119
|
+
invokeChild,
|
|
120
|
+
...(opts?.getCachedOutput ? { getCachedOutput: opts.getCachedOutput } : {}),
|
|
121
|
+
onStepStarted: async (idx, name) => {
|
|
122
|
+
hooks.onStepStart?.(name);
|
|
123
|
+
await opts?.onStepStarted?.(idx, name);
|
|
124
|
+
},
|
|
125
|
+
onStepCompleted: async (idx, name, output, durationMs) => {
|
|
126
|
+
hooks.onStepComplete?.(name, durationMs);
|
|
127
|
+
await opts?.onStepCompleted?.(idx, name, output, durationMs);
|
|
128
|
+
},
|
|
129
|
+
onStepFailed: async (idx, name, err, durationMs) => {
|
|
130
|
+
hooks.onStepFail?.(name, durationMs, formatError(err));
|
|
131
|
+
await opts?.onStepFailed?.(idx, name, err, durationMs);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return { response: (result.output ?? null) as TOutput | null };
|
|
101
135
|
} catch (err) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (workflowErr !== undefined) {
|
|
106
|
-
if (workflowErr instanceof WorkflowError) throw workflowErr;
|
|
107
|
-
throw new WorkflowError(formatError(workflowErr), { cause: workflowErr });
|
|
136
|
+
if (err instanceof WorkflowError) throw err;
|
|
137
|
+
throw new WorkflowError(formatError(err), { cause: err });
|
|
108
138
|
}
|
|
109
|
-
return { response: (wfOutput ?? null) as T | null };
|
|
110
139
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AgentComposeClient } from "../client.js";
|
|
2
|
+
import type { WorkflowCtx } from "../types/workflow.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build the public-API child workflow invoker used by legacy and sandboxed
|
|
6
|
+
* workflow execution. Provider-backed engines may inject a different
|
|
7
|
+
* implementation (Temporal child workflow, Inngest invoke, etc.).
|
|
8
|
+
*/
|
|
9
|
+
export function buildInvokeChild(
|
|
10
|
+
runId: string,
|
|
11
|
+
opts: { fallbackBaseUrl?: string; defaultFactorySlug?: string } = {},
|
|
12
|
+
): WorkflowCtx["invokeChild"] {
|
|
13
|
+
let childClient: AgentComposeClient | null = null;
|
|
14
|
+
const getChildClient = (): AgentComposeClient => {
|
|
15
|
+
if (childClient) return childClient;
|
|
16
|
+
const baseUrl = process.env.AGENT_COMPOSE_URL ?? opts.fallbackBaseUrl;
|
|
17
|
+
const apiKey = process.env.AGENT_COMPOSE_API_KEY;
|
|
18
|
+
if (!baseUrl || !apiKey) {
|
|
19
|
+
throw new Error("ctx.invokeChild requires AGENT_COMPOSE_URL and AGENT_COMPOSE_API_KEY");
|
|
20
|
+
}
|
|
21
|
+
childClient = new AgentComposeClient(baseUrl, apiKey);
|
|
22
|
+
return childClient;
|
|
23
|
+
};
|
|
24
|
+
return (name, input, childOpts) => getChildClient().invokeAndWait(name, input, {
|
|
25
|
+
...childOpts,
|
|
26
|
+
parentRunId: runId,
|
|
27
|
+
factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default",
|
|
28
|
+
});
|
|
29
|
+
}
|