@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.
Files changed (88) hide show
  1. package/README.md +145 -33
  2. package/dist/agent/agent-loop.d.ts +83 -5
  3. package/dist/agent/run-agent.d.ts +34 -9
  4. package/dist/client.d.ts +247 -99
  5. package/dist/index.d.ts +26 -11
  6. package/dist/index.js +1967 -745
  7. package/dist/processors/builtins.d.ts +35 -0
  8. package/dist/processors/index.d.ts +4 -0
  9. package/dist/processors/processor.d.ts +91 -0
  10. package/dist/processors/processor.test.d.ts +1 -0
  11. package/dist/processors/runner.d.ts +19 -0
  12. package/dist/request-context/index.d.ts +2 -0
  13. package/dist/request-context/request-context.d.ts +159 -0
  14. package/dist/request-context/request-context.test.d.ts +1 -0
  15. package/dist/runtimes/claude.d.ts +27 -50
  16. package/dist/runtimes/openai-desktop.js +1918 -741
  17. package/dist/runtimes/vercel.d.ts +34 -0
  18. package/dist/runtimes/vercel.js +474 -0
  19. package/dist/sandbox.d.ts +29 -25
  20. package/dist/step-invocation/__tests__/invoker.test.d.ts +1 -0
  21. package/dist/step-invocation/__tests__/protocol.test.d.ts +1 -0
  22. package/dist/step-invocation/__tests__/server.test.d.ts +1 -0
  23. package/dist/step-invocation/index.d.ts +25 -0
  24. package/dist/step-invocation/invoker.d.ts +65 -0
  25. package/dist/step-invocation/protocol.d.ts +44 -0
  26. package/dist/step-invocation/server.d.ts +63 -0
  27. package/dist/step-invocation/types.d.ts +72 -0
  28. package/dist/tools/coding.d.ts +49 -0
  29. package/dist/tools/coding.test.d.ts +1 -0
  30. package/dist/tools/index.d.ts +2 -0
  31. package/dist/types/events.d.ts +36 -0
  32. package/dist/types/execution-context.d.ts +22 -0
  33. package/dist/types/runtime.d.ts +32 -0
  34. package/dist/types/sandbox-environment.d.ts +5 -2
  35. package/dist/types/sandbox.d.ts +14 -12
  36. package/dist/types/workflow-metadata.d.ts +51 -0
  37. package/dist/types/workflow-plan.d.ts +19 -0
  38. package/dist/types/workflow.d.ts +57 -17
  39. package/dist/utils/bundler.d.ts +62 -3
  40. package/dist/workflow-steps/__tests__/observability.test.d.ts +1 -0
  41. package/dist/workflow-steps/index.d.ts +10 -0
  42. package/dist/workflow-steps/observability.d.ts +58 -0
  43. package/dist/workflow-steps/runner.d.ts +96 -0
  44. package/dist/workflow-steps/step.d.ts +25 -0
  45. package/dist/workflow-steps/types.d.ts +135 -0
  46. package/dist/workflow-steps/workflow-steps.test.d.ts +1 -0
  47. package/dist/workflow-steps/workflow.d.ts +50 -0
  48. package/dist/workflows/engine.d.ts +27 -13
  49. package/dist/workflows/invoke-child.d.ts +10 -0
  50. package/package.json +25 -15
  51. package/src/agent/agent-loop.ts +197 -26
  52. package/src/agent/run-agent.ts +40 -15
  53. package/src/client.ts +326 -76
  54. package/src/index.ts +124 -10
  55. package/src/processors/builtins.ts +72 -0
  56. package/src/processors/index.ts +15 -0
  57. package/src/processors/processor.ts +103 -0
  58. package/src/processors/runner.ts +42 -0
  59. package/src/request-context/index.ts +17 -0
  60. package/src/request-context/request-context.ts +302 -0
  61. package/src/runtimes/claude.ts +123 -254
  62. package/src/runtimes/vercel.ts +180 -0
  63. package/src/sandbox.ts +53 -21
  64. package/src/step-invocation/index.ts +33 -0
  65. package/src/step-invocation/invoker.ts +204 -0
  66. package/src/step-invocation/protocol.ts +57 -0
  67. package/src/step-invocation/server.ts +184 -0
  68. package/src/step-invocation/types.ts +70 -0
  69. package/src/tools/coding.ts +126 -0
  70. package/src/tools/index.ts +8 -0
  71. package/src/types/events.ts +40 -0
  72. package/src/types/execution-context.ts +30 -0
  73. package/src/types/runtime.ts +24 -0
  74. package/src/types/sandbox-environment.ts +7 -5
  75. package/src/types/sandbox.ts +16 -12
  76. package/src/types/workflow-metadata.ts +84 -0
  77. package/src/types/workflow-plan.ts +24 -0
  78. package/src/types/workflow.ts +139 -25
  79. package/src/utils/bundler.ts +198 -18
  80. package/src/utils/source-loader.ts +2 -2
  81. package/src/workflow-steps/index.ts +30 -0
  82. package/src/workflow-steps/observability.ts +103 -0
  83. package/src/workflow-steps/runner.ts +244 -0
  84. package/src/workflow-steps/step.ts +38 -0
  85. package/src/workflow-steps/types.ts +134 -0
  86. package/src/workflow-steps/workflow.ts +95 -0
  87. package/src/workflows/engine.ts +69 -40
  88. 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
+ }
@@ -1,20 +1,23 @@
1
1
  /**
2
- * Workflow engine — runs a workflow function with a minimal ctx.
2
+ * Workflow engine — drives a `Workflow`'s steps.
3
3
  *
4
- * Post de-broker there is no agent-spawning primitive here. Workflows that
5
- * want to embed an LLM agent call `runAgent(opts)` directly from
6
- * `@agent-compose/sdk`. Workflows that want to invoke OTHER workflows use
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
- * - build the `ctx` the workflow function receives
10
- * - funnel `onStepStart / onStepComplete / onStepFail` hooks
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, WorkflowFn } from "../types/workflow.js";
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
- export async function runWorkflow<T = unknown>(
74
- wf: WorkflowFn<T>,
75
- ctx: Pick<WorkflowCtx, "run" | "input">,
76
- opts?: { hooks?: WorkflowHooks; setMetadata?: (data: Record<string, unknown>) => Promise<void> },
77
- ): Promise<WorkflowResult<T>> {
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 setMetadata = opts?.setMetadata ?? (async () => {});
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
- wfOutput = await wf(
89
- {
90
- ...ctx,
91
- setMetadata,
92
- step: async <S>(name: string, fn: () => Promise<S>): Promise<S> => {
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
- workflowErr = err;
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
+ }