@agent-compose/sdk 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +66 -39
  2. package/dist/agent/__tests__/runtime-json-schema.test.d.ts +10 -0
  3. package/dist/agent/agent-context.d.ts +21 -1
  4. package/dist/agent/agent-loop.d.ts +24 -1
  5. package/dist/client.d.ts +338 -534
  6. package/dist/directives.d.ts +112 -0
  7. package/dist/display.d.ts +242 -0
  8. package/dist/errors.d.ts +24 -1
  9. package/dist/index.d.ts +34 -13
  10. package/dist/index.js +2984 -861
  11. package/dist/pause/wrappers.d.ts +31 -9
  12. package/dist/processors/ask-human.d.ts +30 -0
  13. package/dist/processors/ask-human.test.d.ts +1 -0
  14. package/dist/processors/index.d.ts +1 -0
  15. package/dist/runtimes/_acp-client.d.ts +46 -1
  16. package/dist/runtimes/_cli-agent.d.ts +58 -4
  17. package/dist/runtimes/_jsonl-guard.d.ts +103 -0
  18. package/dist/runtimes/amp.d.ts +2 -2
  19. package/dist/runtimes/claude-code.d.ts +59 -0
  20. package/dist/runtimes/claude-code.test.d.ts +14 -0
  21. package/dist/runtimes/claude.d.ts +16 -0
  22. package/dist/runtimes/claude.test.d.ts +8 -0
  23. package/dist/runtimes/codex.d.ts +9 -3
  24. package/dist/runtimes/cursor.d.ts +9 -0
  25. package/dist/runtimes/droid.d.ts +9 -0
  26. package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
  27. package/dist/runtimes/openai-desktop.js +2922 -861
  28. package/dist/runtimes/opencode.d.ts +25 -0
  29. package/dist/runtimes/vercel.js +22 -1
  30. package/dist/sandbox/devbox.d.ts +42 -0
  31. package/dist/sandbox/exec-stream.d.ts +14 -0
  32. package/dist/sandbox/network-policy.d.ts +100 -0
  33. package/dist/sandbox/provider-def.d.ts +79 -0
  34. package/dist/sandbox/providers/desktop.d.ts +10 -0
  35. package/dist/sandbox/providers/e2b.d.ts +17 -0
  36. package/dist/sandbox/providers/local.d.ts +11 -0
  37. package/dist/sandbox/providers/vercel.d.ts +18 -0
  38. package/dist/sandbox/registry.d.ts +45 -0
  39. package/dist/sandbox/sizes.d.ts +68 -0
  40. package/dist/sandbox.d.ts +24 -299
  41. package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
  42. package/dist/step-invocation/invoker.d.ts +24 -1
  43. package/dist/step-invocation/protocol.d.ts +13 -0
  44. package/dist/types/api-compliance.d.ts +71 -0
  45. package/dist/types/api-conversations.d.ts +492 -0
  46. package/dist/types/api-factory.d.ts +309 -0
  47. package/dist/types/api-projects.d.ts +131 -0
  48. package/dist/types/api-runs.d.ts +377 -0
  49. package/dist/types/api-scopes.d.ts +102 -0
  50. package/dist/types/conversation-stream.d.ts +191 -0
  51. package/dist/types/execution-context.d.ts +12 -2
  52. package/dist/types/protocol.d.ts +30 -1
  53. package/dist/types/sandbox-environment.d.ts +8 -5
  54. package/dist/types/sandbox.d.ts +79 -0
  55. package/dist/types/workflow-metadata.d.ts +33 -8
  56. package/dist/types/workflow-plan.d.ts +10 -0
  57. package/dist/types/workflow.d.ts +18 -193
  58. package/dist/utils/bundler.d.ts +12 -1
  59. package/dist/utils/errors.d.ts +9 -1
  60. package/dist/workflow-steps/index.d.ts +1 -1
  61. package/dist/workflow-steps/observability.d.ts +8 -1
  62. package/dist/workflow-steps/runner.d.ts +3 -3
  63. package/dist/workflow-steps/step.d.ts +15 -1
  64. package/dist/workflow-steps/types.d.ts +19 -5
  65. package/dist/workflow-steps/workflow.d.ts +22 -1
  66. package/dist/workflows/engine.d.ts +3 -2
  67. package/dist/workflows/invoke-child.d.ts +2 -2
  68. package/package.json +1 -1
  69. package/src/agent/agent-context.ts +206 -16
  70. package/src/agent/agent-loop.ts +40 -4
  71. package/src/agent/run-agent.ts +9 -1
  72. package/src/client.ts +909 -621
  73. package/src/directives.ts +184 -0
  74. package/src/display.ts +788 -0
  75. package/src/errors.ts +39 -0
  76. package/src/index.ts +117 -10
  77. package/src/pause/wrappers.ts +44 -9
  78. package/src/processors/ask-human.ts +136 -0
  79. package/src/processors/index.ts +5 -0
  80. package/src/runtimes/_acp-client.ts +72 -3
  81. package/src/runtimes/_cli-agent.ts +171 -38
  82. package/src/runtimes/_jsonl-guard.ts +219 -0
  83. package/src/runtimes/claude-code.ts +246 -0
  84. package/src/runtimes/claude.ts +32 -2
  85. package/src/runtimes/codex.ts +55 -3
  86. package/src/runtimes/cursor.ts +59 -0
  87. package/src/runtimes/droid.ts +63 -0
  88. package/src/runtimes/openai-desktop.ts +59 -14
  89. package/src/runtimes/opencode.ts +61 -0
  90. package/src/sandbox/devbox.ts +48 -0
  91. package/src/sandbox/exec-stream.ts +48 -0
  92. package/src/sandbox/network-policy.ts +181 -0
  93. package/src/sandbox/provider-def.ts +94 -0
  94. package/src/sandbox/providers/desktop.ts +57 -0
  95. package/src/sandbox/providers/e2b.ts +354 -0
  96. package/src/sandbox/providers/local.ts +106 -0
  97. package/src/sandbox/providers/vercel.ts +331 -0
  98. package/src/sandbox/registry.ts +198 -0
  99. package/src/sandbox/sizes.ts +95 -0
  100. package/src/sandbox.ts +59 -1263
  101. package/src/step-invocation/invoker.ts +319 -34
  102. package/src/step-invocation/protocol.ts +19 -0
  103. package/src/types/api-compliance.ts +79 -0
  104. package/src/types/api-conversations.ts +522 -0
  105. package/src/types/api-factory.ts +336 -0
  106. package/src/types/api-projects.ts +140 -0
  107. package/src/types/api-runs.ts +412 -0
  108. package/src/types/api-scopes.ts +102 -0
  109. package/src/types/conversation-stream.ts +231 -0
  110. package/src/types/execution-context.ts +10 -2
  111. package/src/types/protocol.ts +33 -0
  112. package/src/types/sandbox-environment.ts +28 -9
  113. package/src/types/sandbox.ts +78 -0
  114. package/src/types/workflow-metadata.ts +35 -8
  115. package/src/types/workflow-plan.ts +11 -0
  116. package/src/types/workflow.ts +25 -280
  117. package/src/utils/bundler.ts +32 -5
  118. package/src/utils/errors.ts +34 -2
  119. package/src/workflow-steps/index.ts +1 -0
  120. package/src/workflow-steps/observability.ts +19 -8
  121. package/src/workflow-steps/runner.ts +4 -4
  122. package/src/workflow-steps/step.ts +49 -1
  123. package/src/workflow-steps/types.ts +20 -5
  124. package/src/workflow-steps/workflow.ts +22 -1
  125. package/src/workflows/engine.ts +3 -2
  126. package/src/workflows/invoke-child.ts +2 -2
@@ -1,45 +1,33 @@
1
1
  /**
2
- * Workflow types — the context and function signature for authoring workflows.
2
+ * Workflow authoring types — `defineWorkflow` and the shared author-facing
3
+ * type surface.
3
4
  *
4
- * A workflow is `async (ctx, sandbox) => T`. Two positional args by design:
5
- *
6
- * - `ctx` carries facts + observability about THIS run (id, input,
7
- * setMetadata, step). Metadata bag.
8
- * - `sandbox` is a capability handed to you by the engine for doing work
9
- * (exec commands, write files). Pass it to `agent({ sandbox, ... })`
10
- * and to any helper that takes a SandboxProvider (git utilities, file
11
- * writers). Constructed once per run; reuse it.
5
+ * Workflows are STEP-FORM only: a builder of discrete, durable steps —
6
+ * `defineWorkflow({ id, input, output }).step(defineStep(...)).build()`.
7
+ * Every step is a replay checkpoint; pause/resume works at step
8
+ * granularity. The legacy run-form (`defineWorkflow({ run(ctx, sandbox)
9
+ * { … } })`) has been removed — passing a `run` key throws at
10
+ * definition time (i.e. at registration, loud and early).
12
11
  *
13
12
  * LLM agent loops live in `agent(opts)` (sdk/src/agent/run-agent.ts).
14
13
  * Invoking other workflows uses `AgentComposeClient.invoke[AndWait](...)`.
15
14
  */
16
15
 
17
- import { z } from "zod";
18
- import type { SandboxNetworkPolicy } from "../sandbox.js";
19
- import type { SandboxProvider } from "./sandbox.js";
20
16
  import type { AgentLifecycleEvent } from "../agent/agent-loop.js";
21
- import type { Processor } from "../processors/processor.js";
22
17
  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, type ConnectorRequirements, type ConnectorOperationTag, type InvokePolicy } from "./workflow-metadata.js";
18
+ import type { StepWorkflowDefinition, WorkflowBuilder } from "../workflow-steps/workflow.js";
28
19
 
29
20
  export type { WorkflowRun, InvokeChild, BaseExecutionContext } from "./execution-context.js";
30
21
  // Re-export so consumers can keep importing `WorkflowMetadata` from
31
22
  // `@agent-compose/sdk` — the canonical definition lives in
32
23
  // `./workflow-metadata.js` to break a runtime import cycle with
33
24
  // `workflow-steps/workflow.ts`.
34
- export type { WorkflowMetadata } from "./workflow-metadata.js";
25
+ export type { WorkflowMetadata, SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources } from "./workflow-metadata.js";
35
26
 
36
27
  export interface AgentEventSink {
37
28
  emit(event: AgentLifecycleEvent): void | Promise<void>;
38
29
  }
39
30
 
40
- import type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources } from "./workflow-metadata.js";
41
- export type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources };
42
-
43
31
  /** Turn/iteration budget for `agent(opts)`. Re-exported here so authors
44
32
  * can type per-invoke budget overrides they pass as workflow input. */
45
33
  export interface AgentBudget {
@@ -47,270 +35,27 @@ export interface AgentBudget {
47
35
  maxIterations: number;
48
36
  }
49
37
 
50
- /** Context passed to a workflow function — facts + observability for this run. */
51
- export interface WorkflowCtx<TInput extends Record<string, unknown> = Record<string, unknown>> extends Omit<BaseExecutionContext, "sandbox"> {
52
- input?: TInput;
53
- /** Persist key-value metadata on the run record (e.g. prUrl, planUrl). */
54
- setMetadata: (data: Record<string, unknown>) => Promise<void>;
55
- /**
56
- * Durable named step (ADR-0012). Runs `fn` once and memoises its result to
57
- * the sandbox state-dir; on a pause-resume re-entry the recorded value is
58
- * returned and `fn` is NOT re-run (a duration-0 "restored" sub-step). Also
59
- * emits substep_started / substep_completed / substep_failed lifecycle
60
- * events with duration — use for long phases you want both durable and
61
- * visible on the run's timeline (setup, external API calls, submit).
62
- *
63
- * Names must be unique within a step body (they key the memoise file).
64
- * A body that pauses is never memoised — the resume re-runs it. For
65
- * cross-process side effects (DB writes, emails) use `invokeChild`.
66
- */
67
- step<T>(name: string, fn: () => Promise<T>): Promise<T>;
68
- /** Pass to `agent({ events: ctx.agentEvents })` to stream agent lifecycle events. */
69
- agentEvents: AgentEventSink;
70
- /**
71
- * Workflow-level processors registered via `defineWorkflow({ processors })`.
72
- * Read-only here. Authors merge with agent-specific lists when calling
73
- * `agent({ processors: [...ctx.processors, mySpecific] })`.
74
- *
75
- * Empty array when the workflow declared no processors.
76
- */
77
- processors: readonly Processor[];
78
- }
79
-
80
- /** A workflow is `async (ctx, sandbox) => TOutput`. */
81
- export type WorkflowFn<
82
- TOutput = unknown,
83
- TInput extends Record<string, unknown> = Record<string, unknown>,
84
- > = (ctx: WorkflowCtx<TInput>, sandbox: SandboxProvider) => Promise<TOutput>;
85
-
86
- /**
87
- * Declare a workflow with server-side metadata.
88
- * Use `export default defineWorkflow({ run, networkPolicy, ... })` to attach
89
- * a runner network policy so the server brokers credentials for the workflow
90
- * sandbox.
91
- *
92
- * Without defineWorkflow, a plain `export default async (ctx) => {...}` still
93
- * works — the workflow just runs with secrets passed directly in env.
94
- */
95
- export interface WorkflowDefinition<
96
- TOutput = unknown,
97
- TInput extends Record<string, unknown> = Record<string, unknown>,
98
- > {
99
- /** One-line, human-readable description of what this workflow does.
100
- * Surfaced on the dashboard template tile + run page header. */
101
- description?: string;
102
- /** Zod schema for the workflow's `input`. When declared, the SDK
103
- * bundler captures it as JSON-Schema-shaped `inputSchema` in
104
- * template metadata, the dashboard playground renders a typed
105
- * form, and the engine validates dispatched payloads against it
106
- * at the step boundary. Omit to leave inputs as `unknown` (the
107
- * legacy default — playground falls back to a freeform JSON
108
- * textarea). */
109
- input?: z.ZodType<TInput>;
110
- /** Same as `input`, for the workflow's return value. Captured into
111
- * `outputSchema` metadata and rendered in the IO panel. */
112
- output?: z.ZodType<TOutput>;
113
- run: WorkflowFn<TOutput, TInput>;
114
- /**
115
- * All snapshot config — boot source plus capture mode.
116
- *
117
- * `snapshots.bootFrom`: the runner restores from this exact provider
118
- * snapshot id at run start. Omit to boot a fresh sandbox.
119
- *
120
- * `snapshots.saveLatest`: `true` captures one snapshot after each
121
- * successful step (latest-only — prior is freed).
122
- * `{ saveLatest: true, retainSteps: true }` keeps every step's
123
- * snapshot for fork / replay / time-travel.
124
- *
125
- * Snapshots are long-lived (never auto-expire). List + delete via
126
- * `agentc snapshot list/delete`. Per-invocation
127
- * `invoke({ snapshots })` overrides this default.
128
- */
129
- snapshots?: SnapshotConfig;
130
- /**
131
- * Sandbox resources — machine SKU (`size`, a `SandboxSize` vCPU string:
132
- * `2vcpu-4gb` | `4vcpu-8gb` | `8vcpu-16gb` | `32vcpu-64gb`) and `provider`
133
- * (`vercel` | `e2b`). `size` maps to provider machine specs at create
134
- * (Vercel: vCPUs, 2048 MB RAM per vCPU); E2B sizing is template-defined and
135
- * ignores it. Optional; omit → smallest SKU on the default provider.
136
- * Per-invocation `invoke({ size })` overrides the size.
137
- */
138
- resources?: SandboxResources;
139
- /**
140
- * Outbound network policy for the runner sandbox.
141
- * Use "*": [] to allow all traffic while still injecting headers for specific domains.
142
- * Secret values are referenced as $VARIABLE and resolved from GCP at dispatch time.
143
- * Vercel only — E2B ignores.
144
- *
145
- * @example
146
- * networkPolicy: {
147
- * allow: {
148
- * "*": [],
149
- * "api.github.com": [{ transform: [{ headers: { "Authorization": "basic:$GITHUB_TOKEN" } }] }],
150
- * }
151
- * }
152
- */
153
- networkPolicy?: SandboxNetworkPolicy;
154
- /**
155
- * Workflow-level processor chain — runs around every `agent(...)` loop the
156
- * workflow body launches, ahead of any agent-specific processors. Use for
157
- * org-wide gating (deny destructive tools, require scopes). Authors merge
158
- * with agent-specific lists via `[...ctx.processors, ...]`.
159
- */
160
- processors?: readonly Processor[];
161
- /**
162
- * Optional placeholder env var values for secrets referenced in the network policy.
163
- * By default, brokered secrets are removed from the runner env entirely — the real
164
- * values are only ever present inside the Vercel firewall config, never in the VM.
165
- * Only set this if a tool or SDK validates the env var format on startup before
166
- * making any requests (e.g. some CLIs check that ANTHROPIC_API_KEY looks like a
167
- * real key). The placeholder is a syntactically valid but non-functional stand-in.
168
- *
169
- * @example
170
- * placeholders: {
171
- * ANTHROPIC_API_KEY: `sk-ant-api03-${"a".repeat(95)}`,
172
- * }
173
- */
174
- placeholders?: Record<string, string>;
175
- /**
176
- * Connector requirements (ADR-0007). Declaring a provider makes the
177
- * server resolve an authorized OAuth grant for the calling principal at
178
- * dispatch time and inject a fresh access token at the network layer —
179
- * workflow code talks to the provider API with plain fetch/SDKs and
180
- * never holds the credential.
181
- *
182
- * @example
183
- * connectors: { github: { scopes: ["repo"] } }
184
- */
185
- connectors?: ConnectorRequirements;
186
- /**
187
- * Marks this workflow as a catalogue OPERATION of a connector — e.g.
188
- * the `create-issue` operation of the `github` connector. Pair with
189
- * `description` + `input`/`output` schemas so the operation is fully
190
- * self-describing (MCP-tool-like) to humans and agents browsing the
191
- * connector catalogue.
192
- *
193
- * @example
194
- * connectorOperation: { provider: "github", operation: "create-issue" }
195
- */
196
- connectorOperation?: ConnectorOperationTag;
197
- /**
198
- * Tier-1 invoke ACL (connector credential boundary). When this workflow
199
- * declares `connectors` (it brokers a credential) AND an `invokePolicy`,
200
- * the server evaluates the calling principal against the policy BEFORE
201
- * binding any grant — a caller matching no clause is refused with HTTP
202
- * 403. Has no effect on workflows that declare no connectors. Omit to
203
- * leave the workflow invokable by the whole team.
204
- *
205
- * @example
206
- * connectors: { github: { access: "read" } },
207
- * invokePolicy: { users: "owner", workflows: ["nightly-orchestrator"] }
208
- */
209
- invokePolicy?: InvokePolicy;
210
- /**
211
- * Internal — set by `defineSandboxEnvironment`, not by workflow authors.
212
- * Marks the workflow as an environment build (base-env / agent-env) so the
213
- * server skips mounting the shared factory drive for its runs (#13). See
214
- * `WorkflowMetadata.environmentBuild`.
215
- */
216
- environmentBuild?: boolean;
217
- }
218
-
219
- /**
220
- * Wrap a legacy `(ctx, sandbox) => T` workflow body as a single-step
221
- * `Workflow` so the engine has one shape to drive. The synthesised step
222
- * runs the user's `run` body inline; `ctx.setMetadata` / `ctx.step` /
223
- * `ctx.agentEvents` proxy directly to the underlying `StepContext` hooks,
224
- * which the engine flushes to the run timeline exactly as the legacy
225
- * full-mode runner did.
226
- *
227
- * Lifecycle granularity is workflow-level (one outer "run" step) — the
228
- * `ctx.step("name", fn)` calls inside the body become sub-step events
229
- * under that one step. Authors keep the same source; the dashboard sees
230
- * the same timeline shape.
231
- */
232
- // TODO: collapse run-form into pure sugar over step-form. Run-form
233
- // already compiles to step-form-with-one-step here, so maintaining two
234
- // authoring surfaces is API duplication that periodically drifts (see
235
- // the input/output thread-through bug fixed on 2026-05-20: step-form
236
- // preserved schemas, run-form silently stamped z.unknown()). Cleaner:
237
- // turn `defineWorkflow({ run })` into a thin wrapper that calls the
238
- // step builder with a synthesised `{name:"run", input, output, run}`
239
- // step, then delete this function. Runtime stays identical.
240
- function compileRunForm<TOutput, TInput extends Record<string, unknown>>(
241
- def: WorkflowDefinition<TOutput, TInput>,
242
- metadata: WorkflowMetadata,
243
- ): Workflow<TInput, TOutput> {
244
- const unknownSchema = z.unknown() as z.ZodType<unknown>;
245
- const inputSchema: z.ZodType<TInput> = def.input ?? (unknownSchema as z.ZodType<TInput>);
246
- const outputSchema: z.ZodType<TOutput> = def.output ?? (unknownSchema as z.ZodType<TOutput>);
247
- const step: Step<TInput, TOutput> = {
248
- name: "run",
249
- input: inputSchema,
250
- output: outputSchema,
251
- run: async (stepCtx) => {
252
- // Proxy the legacy WorkflowCtx hooks to the step's collector-backed
253
- // implementations. The engine harvests the snapshot after execute
254
- // returns; nothing here is a no-op.
255
- const workflowCtx: WorkflowCtx<TInput> = {
256
- input: stepCtx.input,
257
- run: stepCtx.run,
258
- requestContext: stepCtx.requestContext,
259
- invokeChild: stepCtx.invokeChild,
260
- setMetadata: stepCtx.setMetadata,
261
- step: stepCtx.step,
262
- agentEvents: stepCtx.agentEvents,
263
- pause: stepCtx.pause,
264
- sleep: stepCtx.sleep,
265
- waitForEvent: stepCtx.waitForEvent,
266
- processors: metadata.processors ?? [],
267
- };
268
- const sandbox = stepCtx.sandbox;
269
- if (!sandbox) throw new Error("legacy run-form workflow requires a sandbox in StepContext");
270
- return def.run(workflowCtx, sandbox);
271
- },
272
- };
273
- const workflow: Workflow<TInput, TOutput> = {
274
- id: "@run-form",
275
- input: inputSchema,
276
- output: outputSchema,
277
- steps: Object.freeze([step]),
278
- metadata,
279
- };
280
- // Brand BEFORE freezing — defineProperty is the only way to set a
281
- // non-enumerable key, and frozen objects refuse it.
282
- Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
283
- return Object.freeze(workflow);
284
- }
285
-
286
38
  /**
287
- * Declare a workflow. Two forms; both return a `Workflow` whose
288
- * `metadata` field carries the server-readable declarations.
289
- *
290
- * Run form — wraps the legacy `(ctx, sandbox) => T` body as a single
291
- * step internally. Lifecycle granularity is workflow-level (one step).
39
+ * Declare a workflow — the typed step builder. Multiple steps with
40
+ * explicit input/output schemas, durability + replay at every step
41
+ * boundary:
292
42
  *
293
- * Step form — the typed builder. Multiple steps with explicit input/
294
- * output schemas, durability + replay at every step boundary.
43
+ * defineWorkflow({ id, input, output }).step(defineStep(...)).build()
295
44
  *
296
- * Both forms produce the same downstream shape: the bundler reads
297
- * `workflow.metadata.networkPolicy` / `placeholders` / etc.; the server
298
- * stores the step plan; runner subprocesses execute one step at a time
299
- * via the StepInvocation seam.
45
+ * The bundler reads `workflow.metadata.networkPolicy` / `placeholders` /
46
+ * etc. off the built `Workflow`; the server stores the step plan; runner
47
+ * subprocesses execute one step at a time via the StepInvocation seam.
300
48
  */
301
- export function defineWorkflow<
302
- TOutput = unknown,
303
- TInput extends Record<string, unknown> = Record<string, unknown>,
304
- >(
305
- def: WorkflowDefinition<TOutput, TInput>,
306
- ): Workflow<TInput, TOutput>;
307
49
  export function defineWorkflow<TInput, TOutput>(
308
50
  def: StepWorkflowDefinition<TInput, TOutput>,
309
- ): import("../workflow-steps/workflow.js").WorkflowBuilder<TInput, TInput>;
310
- export function defineWorkflow<TOutput = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(
311
- def: WorkflowDefinition<TOutput, TInput> | StepWorkflowDefinition<unknown, unknown>,
312
- ): Workflow<TInput, TOutput> | import("../workflow-steps/workflow.js").WorkflowBuilder<unknown, unknown> {
313
- if ("run" in def) return compileRunForm(def, extractMetadata(def));
51
+ ): WorkflowBuilder<TInput, TInput> {
52
+ if ("run" in def) {
53
+ throw new Error(
54
+ "defineWorkflow: the legacy run-form (`defineWorkflow({ run(ctx, sandbox) { … } })`) has been removed. " +
55
+ "Author workflows in step-form — `defineWorkflow({ id, input, output }).step(defineStep(...)).build()` — " +
56
+ "or run /ac:generate-workflow to scaffold the current shape.",
57
+ );
58
+ }
314
59
  return createStepWorkflow(def);
315
60
  }
316
61
 
@@ -25,7 +25,7 @@ import { parse as babelParse } from "@babel/parser";
25
25
  import type { File, ExportDefaultDeclaration, CallExpression, Expression, Statement } from "@babel/types";
26
26
  import { importSourceModule } from "./source-loader.js";
27
27
  import type { SnapshotConfig } from "../types/workflow.js";
28
- import type { IOSchema, OutputSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources } from "../types/workflow-metadata.js";
28
+ import type { IOSchema, OutputSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources, DriveMergePolicy } from "../types/workflow-metadata.js";
29
29
  import type { SandboxNetworkPolicy } from "../sandbox.js";
30
30
  import { isWorkflow } from "../workflow-steps/workflow.js";
31
31
  import type { Workflow } from "../workflow-steps/types.js";
@@ -101,6 +101,9 @@ export interface BundledWorkflow {
101
101
  /** Sandbox resources declared via `defineWorkflow({ resources })` — machine
102
102
  * SKU (`size`) and `provider` (`vercel` | `e2b`). */
103
103
  resources?: SandboxResources;
104
+ /** Run-branch merge policy declared via `defineWorkflow({ mergePolicy })`.
105
+ * Absent ⇒ `"auto"` (today's behaviour). */
106
+ mergePolicy?: DriveMergePolicy;
104
107
  workflowPlan: WorkflowPlan;
105
108
  /** Compact JSON-Schema-shaped description of the workflow's input
106
109
  * type. Extracted from the workflow's declared `input` zod schema
@@ -173,7 +176,7 @@ async function extractFromBundle<T>(
173
176
  * the latter would also accept `defineWorkflowAttacker`. The runtime brand
174
177
  * check is the second gate, but the syntactic check should be tight.
175
178
  *
176
- * `defineSandboxEnvironment` is sugar over `defineWorkflow` (see
179
+ * `defineSandboxEnvironment` is sugar over the step builder (see
177
180
  * `src/types/sandbox-environment.ts`) and is explicitly registerable via
178
181
  * `agentc register setup.ts --build` to capture a snapshot. The AST gate
179
182
  * accepts it for the same reason it accepts `defineWorkflow`: the bundled
@@ -284,6 +287,28 @@ function sha256(text: string): string {
284
287
  return createHash("sha256").update(text, "utf8").digest("hex");
285
288
  }
286
289
 
290
+ /**
291
+ * The registration-time step plan read off an (already evaluated) workflow
292
+ * object. Narrative fields (`summary`, `deliverables`) ride along additively;
293
+ * conditional spreads keep absent fields ABSENT — the plan travels as JSON,
294
+ * so no `undefined` keys. Exported for tests.
295
+ */
296
+ export function extractWorkflowPlan(workflow: Workflow<unknown, unknown>): WorkflowPlan {
297
+ return workflowPlan(workflow.steps.map((step, index) => ({
298
+ index,
299
+ name: step.name,
300
+ ...(step.summary !== undefined ? { summary: step.summary } : {}),
301
+ ...(step.deliverables?.length
302
+ ? {
303
+ deliverables: step.deliverables.map((d) => ({
304
+ path: d.path,
305
+ ...(d.description !== undefined ? { description: d.description } : {}),
306
+ })),
307
+ }
308
+ : {}),
309
+ })));
310
+ }
311
+
287
312
  /** Bundle a workflow from source. */
288
313
  export async function bundleWorkflow(
289
314
  workflowPath: string,
@@ -312,7 +337,7 @@ export async function bundleWorkflow(
312
337
  }
313
338
 
314
339
  const { metadata } = workflow;
315
- const plan: WorkflowPlan = workflowPlan(workflow.steps.map((step, index) => ({ index, name: step.name })));
340
+ const plan = extractWorkflowPlan(workflow);
316
341
  const manifest: WorkflowManifest = {
317
342
  definitionBrand: true,
318
343
  sourceHash: sha256(source),
@@ -341,6 +366,7 @@ export async function bundleWorkflow(
341
366
  ...(outputSchema !== undefined ? { outputSchema } : {}),
342
367
  ...(metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {}),
343
368
  ...(metadata.resources !== undefined ? { resources: metadata.resources } : {}),
369
+ ...(metadata.mergePolicy !== undefined ? { mergePolicy: metadata.mergePolicy } : {}),
344
370
  ...(metadata.connectors !== undefined ? { connectors: metadata.connectors } : {}),
345
371
  ...(metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {}),
346
372
  ...(metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}),
@@ -356,8 +382,9 @@ export async function bundleWorkflow(
356
382
  *
357
383
  * Returns undefined when:
358
384
  * - The schema can't be serialised (corrupt / unknown variant)
359
- * - The schema is effectively `unknown` (the run-form sugar default —
360
- * no meaningful contract to render). */
385
+ * - The schema is effectively `unknown` (e.g. the
386
+ * `defineSandboxEnvironment` sugar's schemas — no meaningful
387
+ * contract to render). */
361
388
  function extractIOSchema(
362
389
  zodSchema: { _zod?: unknown } | unknown,
363
390
  ): IOSchema | undefined {
@@ -1,4 +1,36 @@
1
- /** Convert any thrown value to a string message. */
1
+ /** Convert any thrown value to a string message, INCLUDING its `.cause` chain.
2
+ *
3
+ * Many wrapped errors carry the real reason on `.cause` and only a generic
4
+ * summary on `.message` — the Temporal SDK's "Failed to start Workflow" is the
5
+ * canonical example (its `.cause` is the actual gRPC rejection, e.g. "search
6
+ * attribute X is not defined"). Returning `.message` alone swallowed that, so
7
+ * failures surfaced as opaque one-liners. Walk the chain and join the messages
8
+ * so the root cause is always visible. Cycle-guarded against self-referential
9
+ * `cause` links. */
2
10
  export function formatError(err: unknown): string {
3
- return err instanceof Error ? err.message : String(err);
11
+ if (!(err instanceof Error)) {
12
+ // Parsed-JSON error payloads (a CLI runtime's `{"error":{"message":…}}`,
13
+ // a provider body) are plain objects, and `String({...})` is the literal
14
+ // "[object Object]" — the exact string that reached session transcripts.
15
+ // Prefer the conventional `.message`; otherwise show the JSON itself.
16
+ if (typeof err === "object" && err !== null) {
17
+ const msg = (err as { message?: unknown }).message;
18
+ if (typeof msg === "string" && msg.length > 0) return msg;
19
+ try {
20
+ return JSON.stringify(err) ?? String(err);
21
+ } catch {
22
+ return String(err);
23
+ }
24
+ }
25
+ return String(err);
26
+ }
27
+ const parts: string[] = [err.message];
28
+ const seen = new Set<unknown>([err]);
29
+ let cause: unknown = (err as { cause?: unknown }).cause;
30
+ while (cause != null && !seen.has(cause)) {
31
+ seen.add(cause);
32
+ parts.push(cause instanceof Error ? cause.message : String(cause));
33
+ cause = cause instanceof Error ? (cause as { cause?: unknown }).cause : undefined;
34
+ }
35
+ return parts.join(": ");
4
36
  }
@@ -21,6 +21,7 @@ export type {
21
21
  export type {
22
22
  Step,
23
23
  StepContext,
24
+ StepDeliverable,
24
25
  StepRunResult,
25
26
  Workflow,
26
27
  } from "./types.js";
@@ -56,7 +56,12 @@ export interface SubStepEvent {
56
56
  * empty snapshot (and the wire payload omits the field entirely). */
57
57
  export interface StepObservability {
58
58
  metadata?: Record<string, unknown>;
59
- events?: AgentLifecycleEvent[];
59
+ /** Residual events that failed live delivery, each carrying its ORIGINAL
60
+ * emit-time `seq`. The server keys its idempotency on that seq — the SAME
61
+ * key the live route used — so a live write whose ack was lost collapses
62
+ * on redelivery instead of duplicating. Array position is NOT the seq:
63
+ * acked events are stripped, so indices shift. */
64
+ events?: Array<AgentLifecycleEvent & { seq: number }>;
60
65
  subSteps?: SubStepEvent[];
61
66
  }
62
67
 
@@ -163,11 +168,12 @@ export class StepObservabilityCollector {
163
168
 
164
169
  readonly agentEvents: AgentEventSink = {
165
170
  emit: (event: AgentLifecycleEvent) => {
166
- // `seq` is the event's index in `events`, captured BEFORE push so
167
- // it matches the index the server-side batch flush uses (its
168
- // `.entries()` loop). Same index → same idempotency key on the
169
- // server. The live route's `acceptedSeqs` response uses this seq;
170
- // we strip those from the snapshot in `snapshot()`.
171
+ // `seq` is the event's emit-time index in `events`, captured BEFORE
172
+ // push. It is THE idempotency handle on both delivery paths: the
173
+ // live POST carries it per event, and `snapshot()` stamps it onto
174
+ // every residual event so the server's batch backstop builds the
175
+ // byte-identical key. The live route's `acceptedSeqs` response uses
176
+ // this seq; we strip acked events in `snapshot()`.
171
177
  const seq = this.events.length;
172
178
  this.events.push(event);
173
179
  if (this.liveEmitter) {
@@ -206,8 +212,13 @@ export class StepObservabilityCollector {
206
212
 
207
213
  const hasMetadata = Object.keys(this.metadata).length > 0;
208
214
  const hasSubSteps = this.subSteps.length > 0;
209
- // Filter out ack'd events — the live route already wrote them.
210
- const remainingEvents = this.events.filter((_, idx) => !this.ackedSeqs.has(idx));
215
+ // Filter out ack'd events — the live route already wrote them — and
216
+ // stamp each survivor with its ORIGINAL emit-time seq. Filtering shifts
217
+ // array positions, so the seq must travel explicitly for the server's
218
+ // backstop to reproduce the live path's idempotency key.
219
+ const remainingEvents = this.events
220
+ .map((event, seq) => ({ ...event, seq }))
221
+ .filter(({ seq }) => !this.ackedSeqs.has(seq));
211
222
  const hasEvents = remainingEvents.length > 0;
212
223
 
213
224
  if (!hasMetadata && !hasEvents && !hasSubSteps) return undefined;
@@ -26,7 +26,7 @@ import { z } from "zod";
26
26
  import type { Workflow, StepContext, StepRunResult } from "./types.js";
27
27
  import type { RequestContext } from "../request-context/request-context.js";
28
28
  import type { SandboxProvider } from "../types/sandbox.js";
29
- import type { WorkflowRun, WorkflowCtx } from "../types/workflow.js";
29
+ import type { WorkflowRun, InvokeChild } from "../types/execution-context.js";
30
30
  import { StepObservabilityCollector, type StepObservability } from "./observability.js";
31
31
  import type { LiveAgentEventEmitter } from "./run-callback.js";
32
32
  import { scopedMemoize } from "../pause/checkpoint.js";
@@ -89,7 +89,7 @@ export interface RunWorkflowStepsOpts<TInput, TOutput> {
89
89
  /** Fire before a step runs (after cache check / before input validation). */
90
90
  onStepStarted?(stepIndex: number, stepName: string): void | Promise<void>;
91
91
  /** Child workflow invocation implementation. Defaults to a clear unsupported error. */
92
- invokeChild?: WorkflowCtx["invokeChild"];
92
+ invokeChild?: InvokeChild;
93
93
  /** Optional live-stream emitter for agent lifecycle events. The runner
94
94
  * passes a fetch-based emitter wired to the per-run callback token so
95
95
  * the dashboard sees events as the agent loop produces them; tests
@@ -111,7 +111,7 @@ export interface RunWorkflowSingleStepOpts {
111
111
  requestContext: RequestContext;
112
112
  sandbox?: SandboxProvider;
113
113
  abortSignal?: AbortSignal;
114
- invokeChild?: WorkflowCtx["invokeChild"];
114
+ invokeChild?: InvokeChild;
115
115
  /** Optional live-stream emitter — see `RunWorkflowStepsOpts.liveAgentEventEmitter`. */
116
116
  liveAgentEventEmitter?: LiveAgentEventEmitter;
117
117
  }
@@ -189,7 +189,7 @@ export async function runWorkflowSteps<TInput, TOutput>(
189
189
  ): Promise<RunWorkflowStepsResult<TOutput>> {
190
190
  const { workflow, run, requestContext } = opts;
191
191
  const abortSignal = opts.abortSignal ?? new AbortController().signal;
192
- const invokeChild: WorkflowCtx["invokeChild"] = opts.invokeChild ?? (() => {
192
+ const invokeChild: InvokeChild = opts.invokeChild ?? (() => {
193
193
  throw new Error("StepContext.invokeChild is not configured for this workflow engine");
194
194
  });
195
195
 
@@ -7,32 +7,80 @@
7
7
  * - `output` Zod schema; validated against `run`'s return value
8
8
  * - `run` step body
9
9
  *
10
+ * Optional narrative fields (rendered on the dashboard workflow graph):
11
+ * - `summary` one plain sentence of what the step actually does
12
+ * - `deliverables` files the step promises to produce
13
+ *
10
14
  * Validation is required (not optional) because the durability story rides
11
15
  * on every step boundary being a recordable, replayable JSON value. A step
12
16
  * without a schema is invisible to the engine's persistence layer.
13
17
  *
18
+ * Narrative caps are enforced HERE, at construction — the bundler evaluates
19
+ * the module at registration, so this is the "length-capped at registration"
20
+ * point and the error names the step in the author's own environment. The
21
+ * server's manifest zod re-checks the same caps (untrusted input).
22
+ *
14
23
  * Type inference flows: `defineStep` infers TInput/TOutput from the schemas
15
24
  * so `run(ctx)` is fully typed via `ctx.input` at the call site.
16
25
  */
17
26
 
18
27
  import type { z } from "zod";
19
- import type { Step, StepContext } from "./types.js";
28
+ import type { Step, StepContext, StepDeliverable } from "./types.js";
20
29
 
21
30
  export interface DefineStepOpts<TInput, TOutput> {
22
31
  name: string;
23
32
  input: z.ZodType<TInput>;
24
33
  output: z.ZodType<TOutput>;
25
34
  run(ctx: StepContext<TInput>): TOutput | Promise<TOutput>;
35
+ /** One plain sentence of what the step actually does. 1-200 chars, no
36
+ * control characters (so no newlines). */
37
+ summary?: string;
38
+ /** Files the step promises to produce. At most 8. */
39
+ deliverables?: StepDeliverable[];
26
40
  }
27
41
 
42
+ // C0 controls + DEL — narrative text is one-line prose, never structural.
43
+ // eslint-disable-next-line no-control-regex
44
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
45
+
28
46
  export function defineStep<TInput, TOutput>(
29
47
  opts: DefineStepOpts<TInput, TOutput>,
30
48
  ): Step<TInput, TOutput> {
31
49
  if (!opts.name) throw new Error("defineStep: 'name' is required");
50
+ if (opts.summary !== undefined) {
51
+ if (opts.summary.length === 0 || opts.summary.length > 200 || CONTROL_CHARS.test(opts.summary)) {
52
+ throw new Error(
53
+ `defineStep("${opts.name}"): 'summary' must be one plain sentence, 1-200 characters, no control characters`,
54
+ );
55
+ }
56
+ }
57
+ if (opts.deliverables !== undefined) {
58
+ if (opts.deliverables.length > 8) {
59
+ throw new Error(`defineStep("${opts.name}"): at most 8 deliverables`);
60
+ }
61
+ for (const d of opts.deliverables) {
62
+ if (!d.path || d.path.length > 200 || CONTROL_CHARS.test(d.path)) {
63
+ throw new Error(
64
+ `defineStep("${opts.name}"): each deliverable needs a 'path' of 1-200 characters, no control characters`,
65
+ );
66
+ }
67
+ if (d.description !== undefined && (d.description.length === 0 || d.description.length > 200 || CONTROL_CHARS.test(d.description))) {
68
+ throw new Error(
69
+ `defineStep("${opts.name}"): deliverable descriptions must be 1-200 characters, no control characters`,
70
+ );
71
+ }
72
+ }
73
+ }
74
+ // Conditional spreads keep absent fields ABSENT (no `undefined` keys —
75
+ // matters for the canonical-hash discipline used elsewhere).
32
76
  return {
33
77
  name: opts.name,
34
78
  input: opts.input,
35
79
  output: opts.output,
36
80
  run: opts.run,
81
+ ...(opts.summary !== undefined ? { summary: opts.summary } : {}),
82
+ ...(opts.deliverables !== undefined
83
+ ? { deliverables: Object.freeze(opts.deliverables.map((d) => ({ ...d }))) }
84
+ : {}),
37
85
  };
38
86
  }