@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
@@ -6,23 +6,41 @@
6
6
  * - `ctx` carries facts + observability about THIS run (id, input,
7
7
  * setMetadata, step). Metadata bag.
8
8
  * - `sandbox` is a capability handed to you by the engine for doing work
9
- * (exec commands, write files). Pass it to `runAgent({ sandbox, ... })`
9
+ * (exec commands, write files). Pass it to `agent({ sandbox, ... })`
10
10
  * and to any helper that takes a SandboxProvider (git utilities, file
11
11
  * writers). Constructed once per run; reuse it.
12
12
  *
13
- * LLM agent loops live in `runAgent(opts)` (sdk/src/agent/run-agent.ts).
13
+ * LLM agent loops live in `agent(opts)` (sdk/src/agent/run-agent.ts).
14
14
  * Invoking other workflows uses `AgentComposeClient.invoke[AndWait](...)`.
15
15
  */
16
16
 
17
+ import { z } from "zod";
17
18
  import type { SandboxNetworkPolicy } from "../sandbox.js";
18
19
  import type { SandboxProvider } from "./sandbox.js";
20
+ import type { AgentLifecycleEvent } from "../agent/agent-loop.js";
21
+ import type { Processor } from "../processors/processor.js";
22
+ import { createStepWorkflow } from "../workflow-steps/workflow.js";
23
+ import type { StepWorkflowDefinition } from "../workflow-steps/workflow.js";
24
+ import type { Step, Workflow } from "../workflow-steps/types.js";
25
+ import { WORKFLOW_BRAND } from "../workflow-steps/types.js";
26
+ import type { BaseExecutionContext, WorkflowRun } from "./execution-context.js";
27
+ import { extractMetadata, type WorkflowMetadata } from "./workflow-metadata.js";
19
28
 
20
- /** The identity of this workflow run. */
21
- export interface WorkflowRun {
22
- id: string;
29
+ export type { WorkflowRun, InvokeChild, BaseExecutionContext } from "./execution-context.js";
30
+ // Re-export so consumers can keep importing `WorkflowMetadata` from
31
+ // `@agent-compose/sdk` — the canonical definition lives in
32
+ // `./workflow-metadata.js` to break a runtime import cycle with
33
+ // `workflow-steps/workflow.ts`.
34
+ export type { WorkflowMetadata } from "./workflow-metadata.js";
35
+
36
+ export interface AgentEventSink {
37
+ emit(event: AgentLifecycleEvent): void | Promise<void>;
23
38
  }
24
39
 
25
- /** Turn/iteration budget for `runAgent(opts)`. Re-exported here so authors
40
+ import type { WorkflowMemoryConfig } from "./workflow-metadata.js";
41
+ export type { WorkflowMemoryConfig };
42
+
43
+ /** Turn/iteration budget for `agent(opts)`. Re-exported here so authors
26
44
  * can type per-invoke budget overrides they pass as workflow input. */
27
45
  export interface AgentBudget {
28
46
  turnsPerIteration: number;
@@ -30,9 +48,8 @@ export interface AgentBudget {
30
48
  }
31
49
 
32
50
  /** Context passed to a workflow function — facts + observability for this run. */
33
- export interface WorkflowCtx {
34
- run: WorkflowRun;
35
- input?: Record<string, unknown>;
51
+ export interface WorkflowCtx<TInput extends Record<string, unknown> = Record<string, unknown>> extends Omit<BaseExecutionContext, "sandbox"> {
52
+ input?: TInput;
36
53
  /** Persist key-value metadata on the run record (e.g. prUrl, planUrl). */
37
54
  setMetadata: (data: Record<string, unknown>) => Promise<void>;
38
55
  /**
@@ -41,10 +58,23 @@ export interface WorkflowCtx {
41
58
  * visible on the run's timeline (setup, external API calls, submit).
42
59
  */
43
60
  step<T>(name: string, fn: () => Promise<T>): Promise<T>;
61
+ /** Pass to `agent({ events: ctx.agentEvents })` to stream agent lifecycle events. */
62
+ agentEvents: AgentEventSink;
63
+ /**
64
+ * Workflow-level processors registered via `defineWorkflow({ processors })`.
65
+ * Read-only here. Authors merge with agent-specific lists when calling
66
+ * `agent({ processors: [...ctx.processors, mySpecific] })`.
67
+ *
68
+ * Empty array when the workflow declared no processors.
69
+ */
70
+ processors: readonly Processor[];
44
71
  }
45
72
 
46
- /** A workflow is `async (ctx, sandbox) => T`. */
47
- export type WorkflowFn<T = unknown> = (ctx: WorkflowCtx, sandbox: SandboxProvider) => Promise<T>;
73
+ /** A workflow is `async (ctx, sandbox) => TOutput`. */
74
+ export type WorkflowFn<
75
+ TOutput = unknown,
76
+ TInput extends Record<string, unknown> = Record<string, unknown>,
77
+ > = (ctx: WorkflowCtx<TInput>, sandbox: SandboxProvider) => Promise<TOutput>;
48
78
 
49
79
  /**
50
80
  * Declare a workflow with server-side metadata.
@@ -55,8 +85,11 @@ export type WorkflowFn<T = unknown> = (ctx: WorkflowCtx, sandbox: SandboxProvide
55
85
  * Without defineWorkflow, a plain `export default async (ctx) => {...}` still
56
86
  * works — the workflow just runs with secrets passed directly in env.
57
87
  */
58
- export interface WorkflowDefinition<T = unknown> {
59
- run: WorkflowFn<T>;
88
+ export interface WorkflowDefinition<
89
+ TOutput = unknown,
90
+ TInput extends Record<string, unknown> = Record<string, unknown>,
91
+ > {
92
+ run: WorkflowFn<TOutput, TInput>;
60
93
  /**
61
94
  * Reference to a snapshot the runner should boot from at run start.
62
95
  * Accepts a run UUID, a workflow name, or `name@version` — any workflow
@@ -88,6 +121,13 @@ export interface WorkflowDefinition<T = unknown> {
88
121
  * }
89
122
  */
90
123
  networkPolicy?: SandboxNetworkPolicy;
124
+ /**
125
+ * Workflow-level processor chain — runs around every `agent(...)` loop the
126
+ * workflow body launches, ahead of any agent-specific processors. Use for
127
+ * org-wide gating (deny destructive tools, require scopes). Authors merge
128
+ * with agent-specific lists via `[...ctx.processors, ...]`.
129
+ */
130
+ processors?: readonly Processor[];
91
131
  /**
92
132
  * Optional placeholder env var values for secrets referenced in the network policy.
93
133
  * By default, brokered secrets are removed from the runner env entirely — the real
@@ -102,22 +142,95 @@ export interface WorkflowDefinition<T = unknown> {
102
142
  * }
103
143
  */
104
144
  placeholders?: Record<string, string>;
145
+ /** Workflow Memory extraction. Defaults to "default" when omitted.
146
+ * Set false to skip memory extraction for this workflow, or point at a
147
+ * custom memory workflow once custom extractors are supported. */
148
+ memory?: WorkflowMemoryConfig;
105
149
  }
106
150
 
107
151
  /**
108
- * Attach server-side metadata to a workflow function.
109
- * The returned function is a valid WorkflowFn with metadata fields attached
110
- * for the bundler / registration layer to read.
152
+ * Wrap a legacy `(ctx, sandbox) => T` workflow body as a single-step
153
+ * `Workflow` so the engine has one shape to drive. The synthesised step
154
+ * runs the user's `run` body inline; `ctx.setMetadata` / `ctx.step` /
155
+ * `ctx.agentEvents` proxy directly to the underlying `StepContext` hooks,
156
+ * which the engine flushes to the run timeline exactly as the legacy
157
+ * full-mode runner did.
158
+ *
159
+ * Lifecycle granularity is workflow-level (one outer "run" step) — the
160
+ * `ctx.step("name", fn)` calls inside the body become sub-step events
161
+ * under that one step. Authors keep the same source; the dashboard sees
162
+ * the same timeline shape.
163
+ */
164
+ function compileRunForm<TOutput, TInput extends Record<string, unknown>>(
165
+ def: WorkflowDefinition<TOutput, TInput>,
166
+ metadata: WorkflowMetadata,
167
+ ): Workflow<TInput, TOutput> {
168
+ const unknownSchema = z.unknown() as z.ZodType<unknown>;
169
+ const step: Step<TInput, TOutput> = {
170
+ name: "run",
171
+ input: unknownSchema as z.ZodType<TInput>,
172
+ output: unknownSchema as z.ZodType<TOutput>,
173
+ run: async (stepCtx) => {
174
+ // Proxy the legacy WorkflowCtx hooks to the step's collector-backed
175
+ // implementations. The engine harvests the snapshot after execute
176
+ // returns; nothing here is a no-op.
177
+ const workflowCtx: WorkflowCtx<TInput> = {
178
+ input: stepCtx.input,
179
+ run: stepCtx.run,
180
+ requestContext: stepCtx.requestContext,
181
+ invokeChild: stepCtx.invokeChild,
182
+ setMetadata: stepCtx.setMetadata,
183
+ step: stepCtx.step,
184
+ agentEvents: stepCtx.agentEvents,
185
+ processors: metadata.processors ?? [],
186
+ };
187
+ const sandbox = stepCtx.sandbox;
188
+ if (!sandbox) throw new Error("legacy run-form workflow requires a sandbox in StepContext");
189
+ return def.run(workflowCtx, sandbox);
190
+ },
191
+ };
192
+ const workflow: Workflow<TInput, TOutput> = {
193
+ id: "@run-form",
194
+ input: unknownSchema as z.ZodType<TInput>,
195
+ output: unknownSchema as z.ZodType<TOutput>,
196
+ steps: Object.freeze([step]),
197
+ metadata,
198
+ };
199
+ // Brand BEFORE freezing — defineProperty is the only way to set a
200
+ // non-enumerable key, and frozen objects refuse it.
201
+ Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
202
+ return Object.freeze(workflow);
203
+ }
204
+
205
+ /**
206
+ * Declare a workflow. Two forms; both return a `Workflow` whose
207
+ * `metadata` field carries the server-readable declarations.
208
+ *
209
+ * Run form — wraps the legacy `(ctx, sandbox) => T` body as a single
210
+ * step internally. Lifecycle granularity is workflow-level (one step).
211
+ *
212
+ * Step form — the typed builder. Multiple steps with explicit input/
213
+ * output schemas, durability + replay at every step boundary.
214
+ *
215
+ * Both forms produce the same downstream shape: the bundler reads
216
+ * `workflow.metadata.networkPolicy` / `placeholders` / etc.; the server
217
+ * stores the step plan; runner subprocesses execute one step at a time
218
+ * via the StepInvocation seam.
111
219
  */
112
- export function defineWorkflow<T = unknown>(
113
- def: WorkflowDefinition<T>,
114
- ): WorkflowFn<T> & Pick<WorkflowDefinition, "networkPolicy" | "placeholders" | "snapshot" | "saveSnapshot"> {
115
- return Object.assign(def.run, {
116
- networkPolicy: def.networkPolicy,
117
- placeholders: def.placeholders,
118
- snapshot: def.snapshot,
119
- saveSnapshot: def.saveSnapshot,
120
- });
220
+ export function defineWorkflow<
221
+ TOutput = unknown,
222
+ TInput extends Record<string, unknown> = Record<string, unknown>,
223
+ >(
224
+ def: WorkflowDefinition<TOutput, TInput>,
225
+ ): Workflow<TInput, TOutput>;
226
+ export function defineWorkflow<TInput, TOutput>(
227
+ def: StepWorkflowDefinition<TInput, TOutput>,
228
+ ): import("../workflow-steps/workflow.js").WorkflowBuilder<TInput, TInput>;
229
+ export function defineWorkflow<TOutput = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(
230
+ def: WorkflowDefinition<TOutput, TInput> | StepWorkflowDefinition<unknown, unknown>,
231
+ ): Workflow<TInput, TOutput> | import("../workflow-steps/workflow.js").WorkflowBuilder<unknown, unknown> {
232
+ if ("run" in def) return compileRunForm(def, extractMetadata(def));
233
+ return createStepWorkflow(def);
121
234
  }
122
235
 
123
236
  /** Observability-only lifecycle hooks — passed to the workflow engine, not workflow authors. */
@@ -125,4 +238,5 @@ export interface WorkflowHooks {
125
238
  onStepStart?: (step: string) => void;
126
239
  onStepComplete?: (step: string, durationMs: number) => void;
127
240
  onStepFail?: (step: string, durationMs: number, reason: string) => void;
241
+ onAgentLifecycleEvent?: (event: AgentLifecycleEvent) => void;
128
242
  }
@@ -4,17 +4,65 @@
4
4
  * Shared between the CLI (agentc register) and tests. Uses `Bun.build`
5
5
  * to bundle TypeScript sources into a self-contained ESM module.
6
6
  *
7
- * Post de-broker there are no agent bundles to produce — workflows embed
8
- * their agent loops inline via `runAgent(...)`, and workflows invoke other
9
- * workflows via the public SDK client.
7
+ * Two layers of validation run before the bundle leaves this function:
8
+ * 1. AST check (`assertDefaultExportIsDefineWorkflow`) on the bundled
9
+ * output — proves the default export is a `defineWorkflow(...)` call,
10
+ * not a raw `async function`. Catches harnesses that hand-roll their
11
+ * own register flow without going through `defineWorkflow`.
12
+ * 2. Runtime brand check (`isWorkflow`) — confirms the value the module
13
+ * actually exports carries the `WORKFLOW_BRAND` symbol that
14
+ * `defineWorkflow` / `defineWorkflow(...).build()` stamps. Catches
15
+ * indirection (re-exports, aliasing) that the AST walk can't follow.
16
+ *
17
+ * Both checks happen here, in the user's dev environment. The server NEVER
18
+ * parses or imports user source — it only validates the structured manifest
19
+ * this function returns alongside the bundled bytes, and cross-checks the
20
+ * manifest's `sourceHash` against the source it received.
10
21
  */
11
22
 
23
+ import { createHash } from "node:crypto";
24
+ import { parse as babelParse } from "@babel/parser";
25
+ import type { File, ExportDefaultDeclaration, CallExpression, Expression, Statement } from "@babel/types";
12
26
  import { importSourceModule } from "./source-loader.js";
13
- import type { WorkflowFn, WorkflowDefinition } from "../types/workflow.js";
27
+ import type { WorkflowMemoryConfig } from "../types/workflow.js";
14
28
  import type { SandboxNetworkPolicy } from "../sandbox.js";
29
+ import { isWorkflow } from "../workflow-steps/workflow.js";
30
+ import type { Workflow } from "../workflow-steps/types.js";
31
+ import { workflowPlan, type WorkflowPlan } from "../types/workflow-plan.js";
32
+
33
+ /** Bumped when the manifest contract changes in a way the server should
34
+ * notice. The server pins its manifest schema to this exact value (no
35
+ * `>=` accepted) so a manifest forged with a future version is refused
36
+ * by zod before any other validation runs. */
37
+ export const BUNDLER_VERSION = 1;
38
+
39
+ /**
40
+ * Manifest emitted alongside the bundled source. Fully serialisable JSON;
41
+ * the server validates its shape with zod and never inspects the source
42
+ * bytes themselves. Manifest is bound to source via `sourceHash` so the
43
+ * server can refuse manifests detached from the source they describe.
44
+ */
45
+ export interface WorkflowManifest {
46
+ /** Always `true`. The bundler stamps this only after both validation
47
+ * gates pass; the server's zod schema pins it as `z.literal(true)`. */
48
+ definitionBrand: true;
49
+ /** sha256 hex digest of `source`. The server cross-checks this against
50
+ * `hashSource(body.source)` to bind the manifest to the bytes it describes. */
51
+ sourceHash: string;
52
+ /** Bundler contract version. See `BUNDLER_VERSION`. */
53
+ bundlerVersion: number;
54
+ }
55
+
56
+ export class WorkflowSourceValidationError extends Error {
57
+ constructor(message: string) {
58
+ super(message);
59
+ this.name = "WorkflowSourceValidationError";
60
+ }
61
+ }
15
62
 
16
63
  export interface BundledWorkflow {
17
64
  source: string;
65
+ manifest: WorkflowManifest;
18
66
  networkPolicy?: SandboxNetworkPolicy;
19
67
  placeholders?: Record<string, string>;
20
68
  /** Run UUID, workflow name, or `name@version` referencing the snapshot
@@ -23,6 +71,9 @@ export interface BundledWorkflow {
23
71
  snapshot?: string;
24
72
  /** Default capture-on-success flag from the workflow definition. */
25
73
  saveSnapshot?: boolean;
74
+ workflowPlan: WorkflowPlan;
75
+ /** Workflow Memory extractor config. */
76
+ memory?: WorkflowMemoryConfig;
26
77
  }
27
78
 
28
79
  async function bundle(path: string, label: string): Promise<string> {
@@ -66,30 +117,159 @@ async function extractFromBundle<T>(
66
117
  } catch { return undefined; }
67
118
  }
68
119
 
120
+ /**
121
+ * Bun bundles `import { defineWorkflow }` to a local identifier — usually
122
+ * the exact name, or a numeric suffix like `defineWorkflow2` to disambiguate
123
+ * scopes. Match exactly that pattern, NOT `startsWith("defineWorkflow")` —
124
+ * the latter would also accept `defineWorkflowAttacker`. The runtime brand
125
+ * check is the second gate, but the syntactic check should be tight.
126
+ */
127
+ const DEFINE_WORKFLOW_CALLEE = /^defineWorkflow\d*$/;
128
+
129
+ function isDefineWorkflowCall(node: CallExpression): boolean {
130
+ if (node.callee.type === "Identifier") {
131
+ return DEFINE_WORKFLOW_CALLEE.test(node.callee.name);
132
+ }
133
+ // `defineWorkflow(...).step(...).build()` — walk the call chain back to
134
+ // the leftmost callee; if any step in the chain hands off to a
135
+ // defineWorkflow identifier, accept it.
136
+ if (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") {
137
+ return isDefineWorkflowCall(node.callee.object);
138
+ }
139
+ return false;
140
+ }
141
+
142
+ /**
143
+ * Parse the bundled source and assert the default export is a CallExpression
144
+ * to an identifier named `defineWorkflow` (or to a `.step(...).build()` chain
145
+ * rooted in one). This catches:
146
+ *
147
+ * - Raw TypeScript shipped to the registry (parse fails on type syntax)
148
+ * - `export default async function run(ctx)` (default is not a call)
149
+ * - `export default { run }` etc. (default is not a call)
150
+ * - `export default someFunction` (default is not a defineWorkflow call)
151
+ *
152
+ * The check is intentionally syntactic — it doesn't follow imports or trace
153
+ * through aliases. The runtime brand check (`isWorkflow`) picks up edge
154
+ * cases the AST can't see, so the two layers compose.
155
+ *
156
+ * Exported so the unit tests can exercise this exact code path; do NOT call
157
+ * from outside the SDK — `bundleWorkflow` is the supported entrypoint.
158
+ */
159
+ export function assertDefaultExportIsDefineWorkflow(source: string, label: string): void {
160
+ let ast: File;
161
+ try {
162
+ ast = babelParse(source, {
163
+ sourceType: "module",
164
+ // Bundler output is plain JS; no JSX, no TS. Allow modern syntax
165
+ // (Bun's bundler may still emit `??=`/`?.` etc).
166
+ plugins: [],
167
+ errorRecovery: false,
168
+ });
169
+ } catch (err) {
170
+ const msg = err instanceof Error ? err.message : String(err);
171
+ throw new WorkflowSourceValidationError(
172
+ `Bundled workflow ${label} failed to parse as JavaScript: ${msg}. ` +
173
+ `If you registered raw .ts source, switch to \`agentc register <file.ts>\` so the source is bundled first.`,
174
+ );
175
+ }
176
+
177
+ const defaultExpr = findDefaultExportExpression(ast.program.body);
178
+ if (!defaultExpr) {
179
+ throw new WorkflowSourceValidationError(
180
+ `Bundled workflow ${label} has no default export. ` +
181
+ `Use \`export default defineWorkflow({ run: ... })\`.`,
182
+ );
183
+ }
184
+
185
+ if (defaultExpr.type !== "CallExpression" || !isDefineWorkflowCall(defaultExpr)) {
186
+ throw new WorkflowSourceValidationError(
187
+ `Bundled workflow ${label} default export must be a \`defineWorkflow({...})\` call ` +
188
+ `(or a \`defineWorkflow(...).step(...).build()\` chain). ` +
189
+ `Got: ${defaultExpr.type}. Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk".`,
190
+ );
191
+ }
192
+ }
193
+
194
+ function findDefaultExportExpression(body: Statement[]): Expression | null {
195
+ const defaultDecl = body.find(
196
+ (node): node is ExportDefaultDeclaration => node.type === "ExportDefaultDeclaration",
197
+ );
198
+ if (defaultDecl) {
199
+ return defaultDecl.declaration as Expression;
200
+ }
201
+
202
+ // Bun bundles `export default defineWorkflow(...)` as:
203
+ // var workflow_default = defineWorkflow(...);
204
+ // export { workflow_default as default };
205
+ const exportNamed = body.find((node) => node.type === "ExportNamedDeclaration" &&
206
+ node.specifiers.some((spec) => spec.type === "ExportSpecifier" &&
207
+ spec.exported.type === "Identifier" && spec.exported.name === "default"));
208
+ if (!exportNamed || exportNamed.type !== "ExportNamedDeclaration") return null;
209
+ const spec = exportNamed.specifiers.find((s) => s.type === "ExportSpecifier" &&
210
+ s.exported.type === "Identifier" && s.exported.name === "default");
211
+ if (!spec || spec.type !== "ExportSpecifier" || spec.local.type !== "Identifier") return null;
212
+ const localName = spec.local.name;
213
+
214
+ for (const stmt of body) {
215
+ if (stmt.type !== "VariableDeclaration") continue;
216
+ for (const decl of stmt.declarations) {
217
+ if (decl.id.type === "Identifier" && decl.id.name === localName && decl.init && decl.init.type !== "TSSatisfiesExpression") {
218
+ return decl.init as Expression;
219
+ }
220
+ }
221
+ }
222
+ return null;
223
+ }
224
+
225
+ /** SHA-256 hex digest of a string. Used to bind a manifest to its source. */
226
+ function sha256(text: string): string {
227
+ return createHash("sha256").update(text, "utf8").digest("hex");
228
+ }
229
+
69
230
  /** Bundle a workflow from source. */
70
231
  export async function bundleWorkflow(
71
232
  workflowPath: string,
72
233
  overrides?: { networkPolicy?: SandboxNetworkPolicy; placeholders?: Record<string, string> },
73
234
  ): Promise<BundledWorkflow> {
74
- const source = await bundle(workflowPath, `workflow "${workflowPath}"`);
235
+ const label = `"${workflowPath}"`;
236
+ const source = await bundle(workflowPath, `workflow ${label}`);
237
+
238
+ // Layer 1: AST gate on the bundled output. Cheapest check; rejects raw TS
239
+ // and non-defineWorkflow defaults before we even try to import the bundle.
240
+ assertDefaultExportIsDefineWorkflow(source, label);
75
241
 
76
- type Wf = WorkflowFn & Pick<WorkflowDefinition, "networkPolicy" | "placeholders" | "snapshot" | "saveSnapshot">;
77
- const wfBits = await extractFromBundle<Pick<Wf, "networkPolicy" | "placeholders" | "snapshot" | "saveSnapshot">>(
242
+ // Layer 2: actually load the bundle and check the runtime brand on the
243
+ // default export. Catches indirection (re-exports, aliasing) the AST walk
244
+ // can't follow. Reads metadata + steps from the workflow's own fields.
245
+ const workflow = await extractFromBundle<Workflow<unknown, unknown>>(
78
246
  source, "workflow",
79
- (wf) => wf ? {
80
- networkPolicy: (wf as Wf).networkPolicy,
81
- placeholders: (wf as Wf).placeholders,
82
- snapshot: (wf as Wf).snapshot,
83
- saveSnapshot: (wf as Wf).saveSnapshot,
84
- } : undefined,
247
+ (wf) => (isWorkflow(wf) ? wf : undefined),
85
248
  );
86
- const { networkPolicy, placeholders, snapshot, saveSnapshot } = wfBits ?? {};
249
+ if (!workflow) {
250
+ throw new WorkflowSourceValidationError(
251
+ `Bundled workflow ${label} default export is not a Workflow. ` +
252
+ `Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk" — ` +
253
+ `raw \`export default async function\` is no longer supported.`,
254
+ );
255
+ }
256
+
257
+ const { metadata } = workflow;
258
+ const plan: WorkflowPlan = workflowPlan(workflow.steps.map((step, index) => ({ index, name: step.name })));
259
+ const manifest: WorkflowManifest = {
260
+ definitionBrand: true,
261
+ sourceHash: sha256(source),
262
+ bundlerVersion: BUNDLER_VERSION,
263
+ };
87
264
 
88
265
  return {
89
266
  source,
90
- networkPolicy: overrides?.networkPolicy ?? networkPolicy,
91
- placeholders: overrides?.placeholders ?? placeholders,
92
- ...(snapshot !== undefined ? { snapshot } : {}),
93
- ...(saveSnapshot !== undefined ? { saveSnapshot } : {}),
267
+ manifest,
268
+ workflowPlan: plan,
269
+ networkPolicy: overrides?.networkPolicy ?? metadata.networkPolicy,
270
+ placeholders: overrides?.placeholders ?? metadata.placeholders,
271
+ ...(metadata.snapshot !== undefined ? { snapshot: metadata.snapshot } : {}),
272
+ ...(metadata.saveSnapshot !== undefined ? { saveSnapshot: metadata.saveSnapshot } : {}),
273
+ ...(metadata.memory !== undefined ? { memory: metadata.memory } : {}),
94
274
  };
95
275
  }
@@ -1,5 +1,5 @@
1
1
  import { dirname } from "path";
2
- import { writeFile, unlink, mkdir } from "node:fs/promises";
2
+ import { writeFile, rm, mkdir } from "node:fs/promises";
3
3
 
4
4
  export const TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
5
5
  export const LATEST_VERSION = "$LATEST";
@@ -11,6 +11,6 @@ export async function importSourceModule<T>(
11
11
  await mkdir(dirname(tmpPath), { recursive: true });
12
12
  await writeFile(tmpPath, source, "utf8");
13
13
  const mod = await import(tmpPath) as T;
14
- const cleanup = () => unlink(tmpPath).catch(() => {});
14
+ const cleanup = () => rm(tmpPath, { force: true });
15
15
  return { mod, cleanup };
16
16
  }
@@ -0,0 +1,30 @@
1
+ export { defineStep } from "./step.js";
2
+ export type { DefineStepOpts } from "./step.js";
3
+
4
+ export { createStepWorkflow, isWorkflow } from "./workflow.js";
5
+ export type { StepWorkflowDefinition, WorkflowBuilder } from "./workflow.js";
6
+
7
+ export {
8
+ runWorkflowSteps,
9
+ runWorkflowSingleStep,
10
+ StepValidationError,
11
+ WorkflowInputValidationError,
12
+ WorkflowOutputValidationError,
13
+ } from "./runner.js";
14
+ export type {
15
+ RunWorkflowStepsOpts,
16
+ RunWorkflowStepsResult,
17
+ RunWorkflowSingleStepOpts,
18
+ RunWorkflowSingleStepResult,
19
+ } from "./runner.js";
20
+
21
+ export type {
22
+ Step,
23
+ StepContext,
24
+ StepRunResult,
25
+ Workflow,
26
+ } from "./types.js";
27
+ export { WORKFLOW_BRAND } from "./types.js";
28
+
29
+ export { StepObservabilityCollector } from "./observability.js";
30
+ export type { StepObservability, SubStepEvent } from "./observability.js";
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Per-step observability collector — buffers metadata writes, agent
3
+ * lifecycle events, and `ctx.step("name", fn)` sub-step records during one
4
+ * step's execution. The runner flushes the snapshot through the
5
+ * tokenised stdout sentinel; the activity persists the snapshot to the
6
+ * run's metadata + lifecycle event tables.
7
+ *
8
+ * STREAMING — known gap. `agentEvents` are currently batched at step
9
+ * end. For long-running steps that emit many agent events, this hides
10
+ * progress from the dashboard until the step completes. The follow-up
11
+ * is to bring back a minimal `/internal/runs/:runId/steps/:stepIndex/events`
12
+ * route gated by a per-step JIT-signed token (mint in the activity,
13
+ * stamp into the runner env, verify on receive) so `agentEvents.emit`
14
+ * POSTs in real time. Until then `metadata` and `subSteps` are
15
+ * effectively boundary events (no streaming need) and batching them
16
+ * matches their natural granularity.
17
+ */
18
+
19
+ import type { AgentLifecycleEvent } from "../agent/agent-loop.js";
20
+ import type { AgentEventSink } from "../types/workflow.js";
21
+
22
+ /** One named sub-step (from `ctx.step("name", async () => ...)`).
23
+ * Becomes a `workflow_substep_*` lifecycle event on the run timeline. */
24
+ export interface SubStepEvent {
25
+ name: string;
26
+ startedAt: number;
27
+ durationMs: number;
28
+ status: "completed" | "failed";
29
+ /** Present when status="failed" — the user error's message. */
30
+ error?: string;
31
+ }
32
+
33
+ /** Snapshot of everything the step's observability hooks recorded. All
34
+ * fields are optional so a step that uses none of the hooks produces an
35
+ * empty snapshot (and the wire payload omits the field entirely). */
36
+ export interface StepObservability {
37
+ metadata?: Record<string, unknown>;
38
+ events?: AgentLifecycleEvent[];
39
+ subSteps?: SubStepEvent[];
40
+ }
41
+
42
+ /**
43
+ * Append-only collector bound to a single step's `StepContext`. The
44
+ * step's `setMetadata` / `step` / `agentEvents` properties all point at
45
+ * this instance. After the step finishes, the engine calls `snapshot()`
46
+ * to extract the bundle for transport.
47
+ *
48
+ * Metadata writes merge (later keys win) — matches the legacy
49
+ * `mergeRunMetadata` semantics so authors don't see a behaviour change
50
+ * across migrations.
51
+ */
52
+ export class StepObservabilityCollector {
53
+ private metadata: Record<string, unknown> = {};
54
+ private events: AgentLifecycleEvent[] = [];
55
+ private subSteps: SubStepEvent[] = [];
56
+
57
+ readonly setMetadata = async (data: Record<string, unknown>): Promise<void> => {
58
+ Object.assign(this.metadata, data);
59
+ };
60
+
61
+ readonly step = async <T>(name: string, fn: () => Promise<T>): Promise<T> => {
62
+ const startedAt = Date.now();
63
+ try {
64
+ const result = await fn();
65
+ this.subSteps.push({
66
+ name,
67
+ startedAt,
68
+ durationMs: Date.now() - startedAt,
69
+ status: "completed",
70
+ });
71
+ return result;
72
+ } catch (err) {
73
+ this.subSteps.push({
74
+ name,
75
+ startedAt,
76
+ durationMs: Date.now() - startedAt,
77
+ status: "failed",
78
+ error: err instanceof Error ? err.message : String(err),
79
+ });
80
+ throw err;
81
+ }
82
+ };
83
+
84
+ readonly agentEvents: AgentEventSink = {
85
+ emit: (event: AgentLifecycleEvent) => {
86
+ this.events.push(event);
87
+ },
88
+ };
89
+
90
+ /** Snapshot the accumulated state. Returns `undefined` when nothing
91
+ * was recorded so the wire payload can drop the field entirely. */
92
+ snapshot(): StepObservability | undefined {
93
+ const hasMetadata = Object.keys(this.metadata).length > 0;
94
+ const hasEvents = this.events.length > 0;
95
+ const hasSubSteps = this.subSteps.length > 0;
96
+ if (!hasMetadata && !hasEvents && !hasSubSteps) return undefined;
97
+ const result: StepObservability = {};
98
+ if (hasMetadata) result.metadata = { ...this.metadata };
99
+ if (hasEvents) result.events = [...this.events];
100
+ if (hasSubSteps) result.subSteps = [...this.subSteps];
101
+ return result;
102
+ }
103
+ }