@agent-compose/sdk 0.7.0 → 0.8.1

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 (119) 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 +32 -1
  5. package/dist/agent/run-agent.d.ts +4 -0
  6. package/dist/client.d.ts +382 -534
  7. package/dist/directives.d.ts +112 -0
  8. package/dist/display.d.ts +258 -0
  9. package/dist/errors.d.ts +24 -1
  10. package/dist/index.d.ts +26 -14
  11. package/dist/index.js +3774 -1679
  12. package/dist/pause/wrappers.d.ts +31 -9
  13. package/dist/runtimes/_acp-client.d.ts +46 -1
  14. package/dist/runtimes/_cli-agent.d.ts +51 -4
  15. package/dist/runtimes/_jsonl-guard.d.ts +103 -0
  16. package/dist/runtimes/amp.d.ts +2 -2
  17. package/dist/runtimes/claude-code.d.ts +61 -0
  18. package/dist/runtimes/claude-code.test.d.ts +14 -0
  19. package/dist/runtimes/claude.d.ts +16 -0
  20. package/dist/runtimes/claude.test.d.ts +8 -0
  21. package/dist/runtimes/codex.d.ts +12 -3
  22. package/dist/runtimes/cursor.d.ts +2 -2
  23. package/dist/runtimes/droid.d.ts +2 -2
  24. package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
  25. package/dist/runtimes/openai-desktop.js +3718 -1680
  26. package/dist/runtimes/opencode.d.ts +2 -2
  27. package/dist/runtimes/vercel.js +12 -1
  28. package/dist/sandbox/devbox.d.ts +42 -0
  29. package/dist/sandbox/exec-stream.d.ts +14 -0
  30. package/dist/sandbox/network-policy.d.ts +100 -0
  31. package/dist/sandbox/provider-def.d.ts +79 -0
  32. package/dist/sandbox/providers/desktop.d.ts +10 -0
  33. package/dist/sandbox/providers/e2b.d.ts +17 -0
  34. package/dist/sandbox/providers/local.d.ts +11 -0
  35. package/dist/sandbox/providers/vercel.d.ts +18 -0
  36. package/dist/sandbox/registry.d.ts +45 -0
  37. package/dist/sandbox/sizes.d.ts +68 -0
  38. package/dist/sandbox.d.ts +24 -299
  39. package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
  40. package/dist/step-invocation/invoker.d.ts +10 -0
  41. package/dist/step-invocation/protocol.d.ts +5 -0
  42. package/dist/types/api-compliance.d.ts +71 -0
  43. package/dist/types/api-conversations.d.ts +523 -0
  44. package/dist/types/api-factory.d.ts +334 -0
  45. package/dist/types/api-projects.d.ts +131 -0
  46. package/dist/types/api-runs.d.ts +422 -0
  47. package/dist/types/api-scopes.d.ts +102 -0
  48. package/dist/types/conversation-stream.d.ts +191 -0
  49. package/dist/types/execution-context.d.ts +12 -2
  50. package/dist/types/protocol.d.ts +38 -1
  51. package/dist/types/sandbox-environment.d.ts +8 -5
  52. package/dist/types/sandbox.d.ts +74 -4
  53. package/dist/types/workflow-metadata.d.ts +41 -8
  54. package/dist/types/workflow-plan.d.ts +10 -0
  55. package/dist/types/workflow.d.ts +18 -205
  56. package/dist/utils/bundler.d.ts +68 -1
  57. package/dist/workflow-steps/index.d.ts +1 -1
  58. package/dist/workflow-steps/observability.d.ts +8 -1
  59. package/dist/workflow-steps/runner.d.ts +3 -3
  60. package/dist/workflow-steps/step.d.ts +15 -1
  61. package/dist/workflow-steps/types.d.ts +19 -5
  62. package/dist/workflow-steps/workflow.d.ts +29 -1
  63. package/dist/workflows/engine.d.ts +3 -2
  64. package/dist/workflows/invoke-child.d.ts +20 -2
  65. package/dist/workflows/invoke-child.test.d.ts +9 -0
  66. package/package.json +2 -2
  67. package/src/agent/agent-context.ts +186 -3
  68. package/src/agent/agent-loop.ts +40 -2
  69. package/src/agent/run-agent.ts +5 -0
  70. package/src/client.ts +1048 -625
  71. package/src/directives.ts +184 -0
  72. package/src/display.ts +834 -0
  73. package/src/errors.ts +39 -0
  74. package/src/index.ts +114 -12
  75. package/src/pause/wrappers.ts +44 -9
  76. package/src/runtimes/_acp-client.ts +72 -3
  77. package/src/runtimes/_cli-agent.ts +161 -36
  78. package/src/runtimes/_jsonl-guard.ts +219 -0
  79. package/src/runtimes/claude-code.ts +256 -0
  80. package/src/runtimes/claude.ts +32 -2
  81. package/src/runtimes/codex.ts +63 -3
  82. package/src/runtimes/openai-desktop.ts +59 -14
  83. package/src/sandbox/devbox.ts +48 -0
  84. package/src/sandbox/exec-stream.ts +48 -0
  85. package/src/sandbox/network-policy.ts +181 -0
  86. package/src/sandbox/provider-def.ts +94 -0
  87. package/src/sandbox/providers/desktop.ts +57 -0
  88. package/src/sandbox/providers/e2b.ts +354 -0
  89. package/src/sandbox/providers/local.ts +106 -0
  90. package/src/sandbox/providers/vercel.ts +331 -0
  91. package/src/sandbox/registry.ts +198 -0
  92. package/src/sandbox/sizes.ts +95 -0
  93. package/src/sandbox.ts +59 -1275
  94. package/src/step-invocation/invoker.ts +151 -28
  95. package/src/step-invocation/protocol.ts +8 -0
  96. package/src/types/api-compliance.ts +79 -0
  97. package/src/types/api-conversations.ts +547 -0
  98. package/src/types/api-factory.ts +368 -0
  99. package/src/types/api-projects.ts +140 -0
  100. package/src/types/api-runs.ts +459 -0
  101. package/src/types/api-scopes.ts +102 -0
  102. package/src/types/conversation-stream.ts +231 -0
  103. package/src/types/execution-context.ts +10 -2
  104. package/src/types/protocol.ts +41 -0
  105. package/src/types/sandbox-environment.ts +28 -9
  106. package/src/types/sandbox.ts +73 -4
  107. package/src/types/workflow-metadata.ts +44 -8
  108. package/src/types/workflow-plan.ts +11 -0
  109. package/src/types/workflow.ts +25 -292
  110. package/src/utils/bundler.ts +245 -8
  111. package/src/utils/errors.ts +16 -1
  112. package/src/workflow-steps/index.ts +1 -0
  113. package/src/workflow-steps/observability.ts +19 -8
  114. package/src/workflow-steps/runner.ts +4 -4
  115. package/src/workflow-steps/step.ts +49 -1
  116. package/src/workflow-steps/types.ts +20 -5
  117. package/src/workflow-steps/workflow.ts +29 -1
  118. package/src/workflows/engine.ts +3 -2
  119. package/src/workflows/invoke-child.ts +49 -13
@@ -18,14 +18,28 @@
18
18
  * parses or imports user source — it only validates the structured manifest
19
19
  * this function returns alongside the bundled bytes, and cross-checks the
20
20
  * manifest's `sourceHash` against the source it received.
21
+ *
22
+ * Module resolution carries two more layers, because most workflow source is
23
+ * now written by an AGENT and the bundler's error is the only feedback it
24
+ * gets (the Workflow Studio's agent authored `import … from "agentc/sdk"`
25
+ * and prod answered with bun's `Maybe you need to "bun install"?` — advice
26
+ * nobody could act on inside a sandbox with no package.json):
27
+ *
28
+ * - TOLERATE (`SDK_SPECIFIER_ALIASES` + `sdkAliasPlugin`) — near-miss
29
+ * spellings of `@agent-compose/sdk` resolve to the real package, so a
30
+ * draft already written with the wrong one builds unedited.
31
+ * - SELF-CORRECT (`explainBundleFailure`) — anything that still fails to
32
+ * resolve produces an error NAMING the real package, which an agent
33
+ * reading its own tool error can fix on the next turn.
21
34
  */
22
35
 
23
36
  import { createHash } from "node:crypto";
37
+ import { dirname } from "node:path";
24
38
  import { parse as babelParse } from "@babel/parser";
25
39
  import type { File, ExportDefaultDeclaration, CallExpression, Expression, Statement } from "@babel/types";
26
40
  import { importSourceModule } from "./source-loader.js";
27
41
  import type { SnapshotConfig } from "../types/workflow.js";
28
- import type { IOSchema, OutputSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources } from "../types/workflow-metadata.js";
42
+ import type { IOSchema, OutputSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources, DriveMergePolicy } from "../types/workflow-metadata.js";
29
43
  import type { SandboxNetworkPolicy } from "../sandbox.js";
30
44
  import { isWorkflow } from "../workflow-steps/workflow.js";
31
45
  import type { Workflow } from "../workflow-steps/types.js";
@@ -101,6 +115,9 @@ export interface BundledWorkflow {
101
115
  /** Sandbox resources declared via `defineWorkflow({ resources })` — machine
102
116
  * SKU (`size`) and `provider` (`vercel` | `e2b`). */
103
117
  resources?: SandboxResources;
118
+ /** Run-branch merge policy declared via `defineWorkflow({ mergePolicy })`.
119
+ * Absent ⇒ `"auto"` (today's behaviour). */
120
+ mergePolicy?: DriveMergePolicy;
104
121
  workflowPlan: WorkflowPlan;
105
122
  /** Compact JSON-Schema-shaped description of the workflow's input
106
123
  * type. Extracted from the workflow's declared `input` zod schema
@@ -118,11 +135,191 @@ export interface BundledWorkflow {
118
135
  /** Set by `defineSandboxEnvironment` — marks an environment build so the
119
136
  * server skips the /factory mount for its runs (#13). */
120
137
  environmentBuild?: boolean;
138
+ /** Drive requirement declared via `defineWorkflow({ factoryDrive })`.
139
+ * Absent ⇒ `"required"` (a failed /factory mount fails the run);
140
+ * `"none"` = explicit no-drive opt-out. */
141
+ factoryDrive?: "required" | "none";
142
+ }
143
+
144
+ /** The one true import specifier for the platform SDK. */
145
+ export const SDK_PACKAGE = "@agent-compose/sdk";
146
+
147
+ /**
148
+ * Import specifiers that can only have MEANT `@agent-compose/sdk`, rewritten
149
+ * to it at bundle time so a draft written with the wrong spelling builds
150
+ * unedited.
151
+ *
152
+ * Every entry carries an `sdk` segment or suffix, so none of them can be a
153
+ * real third-party package a workflow might legitimately depend on: `a/b`
154
+ * forms are subpaths of packages that don't exist, and the `*-sdk` forms
155
+ * name this platform explicitly. Bare `agentc` / `agent-compose` are
156
+ * deliberately NOT aliased — those are plausible npm package names, and
157
+ * silently redirecting a real dependency is worse than a clear error.
158
+ *
159
+ * Exported for the unit test that pins the table.
160
+ */
161
+ export const SDK_SPECIFIER_ALIASES: readonly string[] = [
162
+ "agentc/sdk",
163
+ "@agentc/sdk",
164
+ "agentc-sdk",
165
+ "agent-compose/sdk",
166
+ "agentcompose/sdk",
167
+ "@agentcompose/sdk",
168
+ "agent-compose-sdk",
169
+ ];
170
+
171
+ const SDK_ALIAS_SET = new Set(SDK_SPECIFIER_ALIASES);
172
+
173
+ /** `@agent-compose/sdk` when `specifier` is a known near-miss for it, else
174
+ * null. The single authority: the plugin's regex filter is only a fast
175
+ * pre-filter, and this decides. */
176
+ export function resolveSdkAlias(specifier: string): string | null {
177
+ return SDK_ALIAS_SET.has(specifier) ? SDK_PACKAGE : null;
178
+ }
179
+
180
+ /** Minimal shape of the Bun surface this module drives. Accessed via
181
+ * globalThis so the SDK keeps no compile-time dependency on @types/bun. */
182
+ interface BunBuildLog { message: string; name?: string; specifier?: string }
183
+ interface BunSurface {
184
+ build(opts: {
185
+ entrypoints: string[]; format: string; target: string;
186
+ plugins?: { name: string; setup(build: BunPluginBuild): void }[];
187
+ }): Promise<{ success: boolean; outputs: { text(): Promise<string> }[]; logs: BunBuildLog[] }>;
188
+ resolveSync(specifier: string, parent: string): string;
189
+ }
190
+ interface BunPluginBuild {
191
+ onResolve(
192
+ constraints: { filter: RegExp; namespace?: string },
193
+ callback: (args: { path: string; importer?: string; resolveDir?: string }) => { path: string } | undefined,
194
+ ): void;
195
+ }
196
+
197
+ /** Anchored regex over the alias table — the plugin filter. Specifiers are
198
+ * literal package names, but escape anyway so a future entry with a `.`
199
+ * or `+` can't widen the filter. */
200
+ function aliasFilter(): RegExp {
201
+ const alternation = SDK_SPECIFIER_ALIASES
202
+ .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
203
+ .join("|");
204
+ return new RegExp(`^(?:${alternation})$`);
205
+ }
206
+
207
+ /**
208
+ * The TOLERATE layer: a Bun resolve plugin that maps every known near-miss
209
+ * specifier onto the real SDK. Resolution goes through Bun's own resolver
210
+ * from the importing file's directory, so the alias lands on exactly the
211
+ * `@agent-compose/sdk` the build would have used had the source spelled it
212
+ * correctly. When the real SDK can't be resolved either, the plugin declines
213
+ * and Bun's failure flows into `explainBundleFailure` below.
214
+ */
215
+ function sdkAliasPlugin(bun: BunSurface): { name: string; setup(build: BunPluginBuild): void } {
216
+ return {
217
+ name: "agent-compose-sdk-alias",
218
+ setup(build) {
219
+ build.onResolve({ filter: aliasFilter() }, (args) => {
220
+ const target = resolveSdkAlias(args.path);
221
+ if (!target) return undefined;
222
+ // `importer` is the importing FILE, `resolveDir` already a directory.
223
+ const from = args.importer ? dirname(args.importer) : args.resolveDir;
224
+ try {
225
+ return { path: bun.resolveSync(target, from && from.length > 0 ? from : process.cwd()) };
226
+ } catch {
227
+ return undefined;
228
+ }
229
+ });
230
+ },
231
+ };
232
+ }
233
+
234
+ /** The unresolved specifier a Bun diagnostic is about, or null when it isn't
235
+ * a resolve failure. `ResolveMessage` carries it as a field; parsing the
236
+ * message text is the fallback. */
237
+ function readUnresolvedSpecifier(diag: unknown): string | null {
238
+ if (!diag || typeof diag !== "object") return null;
239
+ const d = diag as { specifier?: unknown; message?: unknown };
240
+ if (typeof d.specifier === "string" && d.specifier.length > 0) return d.specifier;
241
+ const message = typeof d.message === "string" ? d.message : "";
242
+ const m = /Could not resolve:?\s*"([^"]+)"/.exec(message);
243
+ return m ? m[1] : null;
244
+ }
245
+
246
+ /** Flatten a bundler failure into the diagnostics it actually carries.
247
+ * Bun reports through two channels and both land here: a THROWN
248
+ * `AggregateError("Bundle failed")` whose real messages hide in `.errors`,
249
+ * and a returned `logs` array on `success: false`. */
250
+ function flattenDiagnostics(failure: unknown): unknown[] {
251
+ const out: unknown[] = [];
252
+ const seen = new Set<unknown>();
253
+ const walk = (node: unknown, depth: number): void => {
254
+ if (!node || depth > 4 || seen.has(node)) return;
255
+ seen.add(node);
256
+ if (Array.isArray(node)) {
257
+ for (const sub of node) walk(sub, depth + 1);
258
+ return;
259
+ }
260
+ out.push(node);
261
+ if (typeof node === "object") {
262
+ const n = node as { errors?: unknown; cause?: unknown };
263
+ if (Array.isArray(n.errors)) for (const sub of n.errors) walk(sub, depth + 1);
264
+ if (n.cause) walk(n.cause, depth + 1);
265
+ }
266
+ };
267
+ walk(failure, 0);
268
+ return out;
269
+ }
270
+
271
+ function diagnosticText(diag: unknown): string {
272
+ if (diag instanceof Error) return diag.message || String(diag);
273
+ if (diag && typeof diag === "object" && typeof (diag as { message?: unknown }).message === "string") {
274
+ return (diag as { message: string }).message;
275
+ }
276
+ return String(diag);
277
+ }
278
+
279
+ /**
280
+ * The SELF-CORRECTING layer. Turns a Bun bundle failure into a message that
281
+ * names the real fix instead of leaking Bun's internals.
282
+ *
283
+ * An unresolved import is almost always one thing: workflow source naming
284
+ * the platform SDK by a specifier that isn't its package name. Bun answers
285
+ * that with `Could not resolve: "agentc/sdk". Maybe you need to "bun
286
+ * install"?` — advice the author cannot act on (there is no package.json to
287
+ * install into; the drive holds one file). The rewritten message says the
288
+ * package name, so an agent reading its own tool error can fix the import on
289
+ * the next turn.
290
+ *
291
+ * Non-resolve failures (syntax errors, transform failures) keep their
292
+ * verbatim diagnostics — those are already actionable.
293
+ *
294
+ * Exported for the unit test; `bundleWorkflow` is the supported entrypoint.
295
+ */
296
+ export function explainBundleFailure(err: unknown, label: string): string {
297
+ const diagnostics = flattenDiagnostics(err);
298
+ const unresolved: string[] = [];
299
+ for (const diag of diagnostics) {
300
+ const specifier = readUnresolvedSpecifier(diag);
301
+ if (specifier && !unresolved.includes(specifier)) unresolved.push(specifier);
302
+ }
303
+
304
+ if (unresolved.length > 0) {
305
+ const quoted = unresolved.map((s) => `"${s}"`).join(", ");
306
+ return (
307
+ `Could not resolve ${quoted} — workflow source imports the platform SDK as "${SDK_PACKAGE}". ` +
308
+ `Fix the import specifier in ${label}; anything that is genuinely a third-party ` +
309
+ `package must be installed where the source is bundled.`
310
+ );
311
+ }
312
+
313
+ const detail = diagnostics
314
+ .map(diagnosticText)
315
+ .filter((t) => t.length > 0 && t !== "Bundle failed")
316
+ .join("\n");
317
+ return `Failed to bundle ${label}:\n${detail || "the bundler reported no diagnostics"}`;
121
318
  }
122
319
 
123
320
  async function bundle(path: string, label: string): Promise<string> {
124
321
  // Use globalThis to access Bun without a compile-time dependency on @types/bun.
125
- const bun = globalThis as unknown as { Bun?: { build(opts: { entrypoints: string[]; format: string; target: string }): Promise<{ success: boolean; outputs: { text(): Promise<string> }[]; logs: { message: string }[] }> } };
322
+ const bun = globalThis as unknown as { Bun?: BunSurface };
126
323
  if (!bun.Bun?.build) throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
127
324
  // target: "node" — the bundle runs inside the Vercel sandbox under Node 22+.
128
325
  //
@@ -138,9 +335,24 @@ async function bundle(path: string, label: string): Promise<string> {
138
335
  // Pure-ESM workflows (e.g. only importing `@agent-compose/sdk`) bundled
139
336
  // identically under either target — that's why this bug stayed hidden
140
337
  // until the first workflow that pulled CJS deps got dispatched.
141
- const result = await bun.Bun.build({ entrypoints: [path], format: "esm", target: "node" });
338
+ //
339
+ // The alias plugin rewrites known near-miss SDK specifiers before Bun's
340
+ // resolver sees them; whatever still fails to resolve comes back through
341
+ // `explainBundleFailure` naming the real package instead of Bun's
342
+ // "Maybe you need to `bun install`?".
343
+ let result: Awaited<ReturnType<BunSurface["build"]>>;
344
+ try {
345
+ result = await bun.Bun.build({
346
+ entrypoints: [path], format: "esm", target: "node",
347
+ plugins: [sdkAliasPlugin(bun.Bun)],
348
+ });
349
+ } catch (err) {
350
+ // Modern Bun.build THROWS AggregateError("Bundle failed") on a resolve
351
+ // or transform error rather than returning `success: false`.
352
+ throw new WorkflowSourceValidationError(explainBundleFailure(err, label));
353
+ }
142
354
  if (!result.success) {
143
- throw new Error(`Failed to bundle ${label}:\n${result.logs.map((l) => l.message).join("\n")}`);
355
+ throw new WorkflowSourceValidationError(explainBundleFailure(result.logs, label));
144
356
  }
145
357
  return result.outputs[0].text();
146
358
  }
@@ -173,7 +385,7 @@ async function extractFromBundle<T>(
173
385
  * the latter would also accept `defineWorkflowAttacker`. The runtime brand
174
386
  * check is the second gate, but the syntactic check should be tight.
175
387
  *
176
- * `defineSandboxEnvironment` is sugar over `defineWorkflow` (see
388
+ * `defineSandboxEnvironment` is sugar over the step builder (see
177
389
  * `src/types/sandbox-environment.ts`) and is explicitly registerable via
178
390
  * `agentc register setup.ts --build` to capture a snapshot. The AST gate
179
391
  * accepts it for the same reason it accepts `defineWorkflow`: the bundled
@@ -284,6 +496,28 @@ function sha256(text: string): string {
284
496
  return createHash("sha256").update(text, "utf8").digest("hex");
285
497
  }
286
498
 
499
+ /**
500
+ * The registration-time step plan read off an (already evaluated) workflow
501
+ * object. Narrative fields (`summary`, `deliverables`) ride along additively;
502
+ * conditional spreads keep absent fields ABSENT — the plan travels as JSON,
503
+ * so no `undefined` keys. Exported for tests.
504
+ */
505
+ export function extractWorkflowPlan(workflow: Workflow<unknown, unknown>): WorkflowPlan {
506
+ return workflowPlan(workflow.steps.map((step, index) => ({
507
+ index,
508
+ name: step.name,
509
+ ...(step.summary !== undefined ? { summary: step.summary } : {}),
510
+ ...(step.deliverables?.length
511
+ ? {
512
+ deliverables: step.deliverables.map((d) => ({
513
+ path: d.path,
514
+ ...(d.description !== undefined ? { description: d.description } : {}),
515
+ })),
516
+ }
517
+ : {}),
518
+ })));
519
+ }
520
+
287
521
  /** Bundle a workflow from source. */
288
522
  export async function bundleWorkflow(
289
523
  workflowPath: string,
@@ -312,7 +546,7 @@ export async function bundleWorkflow(
312
546
  }
313
547
 
314
548
  const { metadata } = workflow;
315
- const plan: WorkflowPlan = workflowPlan(workflow.steps.map((step, index) => ({ index, name: step.name })));
549
+ const plan = extractWorkflowPlan(workflow);
316
550
  const manifest: WorkflowManifest = {
317
551
  definitionBrand: true,
318
552
  sourceHash: sha256(source),
@@ -341,10 +575,12 @@ export async function bundleWorkflow(
341
575
  ...(outputSchema !== undefined ? { outputSchema } : {}),
342
576
  ...(metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {}),
343
577
  ...(metadata.resources !== undefined ? { resources: metadata.resources } : {}),
578
+ ...(metadata.mergePolicy !== undefined ? { mergePolicy: metadata.mergePolicy } : {}),
344
579
  ...(metadata.connectors !== undefined ? { connectors: metadata.connectors } : {}),
345
580
  ...(metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {}),
346
581
  ...(metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}),
347
582
  ...(metadata.environmentBuild !== undefined ? { environmentBuild: metadata.environmentBuild } : {}),
583
+ ...(metadata.factoryDrive !== undefined ? { factoryDrive: metadata.factoryDrive } : {}),
348
584
  };
349
585
  }
350
586
 
@@ -356,8 +592,9 @@ export async function bundleWorkflow(
356
592
  *
357
593
  * Returns undefined when:
358
594
  * - 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). */
595
+ * - The schema is effectively `unknown` (e.g. the
596
+ * `defineSandboxEnvironment` sugar's schemas — no meaningful
597
+ * contract to render). */
361
598
  function extractIOSchema(
362
599
  zodSchema: { _zod?: unknown } | unknown,
363
600
  ): IOSchema | undefined {
@@ -8,7 +8,22 @@
8
8
  * so the root cause is always visible. Cycle-guarded against self-referential
9
9
  * `cause` links. */
10
10
  export function formatError(err: unknown): string {
11
- if (!(err instanceof Error)) return 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
+ }
12
27
  const parts: string[] = [err.message];
13
28
  const seen = new Set<unknown>([err]);
14
29
  let cause: unknown = (err as { cause?: unknown }).cause;
@@ -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
  }
@@ -15,6 +15,7 @@ import type { z } from "zod";
15
15
  import type { BaseExecutionContext } from "../types/execution-context.js";
16
16
  import type { AgentEventSink } from "../types/workflow.js";
17
17
  import type { WorkflowMetadata } from "../types/workflow-metadata.js";
18
+ import type { StepObservability } from "./observability.js";
18
19
 
19
20
  /**
20
21
  * Per-step execution context. Threaded into every step's `execute(...)` so
@@ -62,6 +63,15 @@ export interface StepContext<TInput = unknown> extends BaseExecutionContext {
62
63
  agentEvents: AgentEventSink;
63
64
  }
64
65
 
66
+ /** One artifact a step promises to produce. `path` is workspace/drive-relative
67
+ * (e.g. "out/report.html"); the dashboard derives the format tag from the
68
+ * extension client-side — no `format` field here. */
69
+ export interface StepDeliverable {
70
+ path: string;
71
+ /** Optional one-line description of the artifact. */
72
+ description?: string;
73
+ }
74
+
65
75
  /**
66
76
  * Step definition — a single typed unit of work in a workflow chain.
67
77
  *
@@ -80,6 +90,11 @@ export interface Step<TInput, TOutput> {
80
90
  readonly output: z.ZodType<TOutput>;
81
91
  /** Step body. Receives a `StepContext<TInput>` and returns the typed output. */
82
92
  run(ctx: StepContext<TInput>): TOutput | Promise<TOutput>;
93
+ /** One plain sentence of what the step actually does — rendered on the
94
+ * dashboard workflow graph. ≤200 chars, no newlines. */
95
+ readonly summary?: string;
96
+ /** Files the step promises to produce. ≤8 entries. */
97
+ readonly deliverables?: readonly StepDeliverable[];
83
98
  }
84
99
 
85
100
  /**
@@ -90,14 +105,14 @@ export interface Step<TInput, TOutput> {
90
105
  * the step; undefined when no hooks were used.
91
106
  */
92
107
  export type StepRunResult<TOutput = unknown> =
93
- | { status: "completed"; output: TOutput; durationMs: number; observability?: import("./observability.js").StepObservability }
94
- | { status: "failed"; error: string; durationMs: number; observability?: import("./observability.js").StepObservability };
108
+ | { status: "completed"; output: TOutput; durationMs: number; observability?: StepObservability }
109
+ | { status: "failed"; error: string; durationMs: number; observability?: StepObservability };
95
110
 
96
111
  /**
97
112
  * Workflow — a list of typed steps plus the workflow's input/output
98
- * schemas plus its server-side metadata bag. Returned by `defineWorkflow(...)`
99
- * (run form) and `defineWorkflow(...).step(...)...build()` (step form).
100
- * Engine adapters consume this shape.
113
+ * schemas plus its server-side metadata bag. Returned by
114
+ * `defineWorkflow(...).step(...)...build()`. Engine adapters consume
115
+ * this shape.
101
116
  *
102
117
  * `input` validates the workflow input before the first step runs.
103
118
  * `output` validates the final step's output before the workflow
@@ -23,7 +23,7 @@ import type { z } from "zod";
23
23
  import type { Step, Workflow } from "./types.js";
24
24
  import { WORKFLOW_BRAND } from "./types.js";
25
25
  import { extractMetadata } from "../types/workflow-metadata.js";
26
- import type { SnapshotConfig, SandboxResources } from "../types/workflow-metadata.js";
26
+ import type { SnapshotConfig, SandboxResources, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, DriveMergePolicy } from "../types/workflow-metadata.js";
27
27
  import type { SandboxNetworkPolicy } from "../sandbox.js";
28
28
  import type { Processor } from "../processors/processor.js";
29
29
 
@@ -55,7 +55,35 @@ export interface StepWorkflowDefinition<TInput, TOutput> {
55
55
  * and `provider` (`vercel` | `e2b`). Vercel maps `size` to vCPUs; E2B
56
56
  * sizing is template-defined. Omit → smallest SKU on the default provider. */
57
57
  resources?: SandboxResources;
58
+ /** What happens to this workflow's drive branch when a run ends:
59
+ * `"auto"` (the default when omitted — today's behaviour) folds it into
60
+ * `main`; `"manual"` proposes a merge approval at the same terminus
61
+ * instead, leaving the branch durable until a human clicks approve. A
62
+ * cancelled run neither merges nor proposes under either policy. */
63
+ mergePolicy?: DriveMergePolicy;
58
64
  processors?: readonly Processor[];
65
+ /** Connector requirements (ADR-0007) — providers whose APIs this workflow
66
+ * calls. Dispatch resolves an authorized grant per provider and injects a
67
+ * fresh access token at the network layer. */
68
+ connectors?: ConnectorRequirements;
69
+ /** Marks this workflow as a catalogue OPERATION of a connector — e.g. the
70
+ * `create-issue` operation of the `github` connector. */
71
+ connectorOperation?: ConnectorOperationTag;
72
+ /** Tier-1 invoke ACL — who may dispatch this connector-brokering workflow.
73
+ * See `InvokePolicy`. */
74
+ invokePolicy?: InvokePolicy;
75
+ /** Internal — set by `defineSandboxEnvironment`, not by workflow authors.
76
+ * Marks the workflow as an environment build (base-env / agent-env) so the
77
+ * server skips mounting the shared factory drive for its runs (#13). See
78
+ * `WorkflowMetadata.environmentBuild`. */
79
+ environmentBuild?: boolean;
80
+ /** Whether this workflow's runs need the factory drive. Omit (⇒
81
+ * `"required"`) for any workflow that reads or writes /factory: a failed
82
+ * mount then FAILS the run instead of silently proceeding drive-less.
83
+ * Declare `"none"` for a drive-agnostic workflow — the server skips the
84
+ * /factory mount entirely for its runs. See
85
+ * `WorkflowMetadata.factoryDrive`. */
86
+ factoryDrive?: "required" | "none";
59
87
  }
60
88
 
61
89
  export function createStepWorkflow<TInput, TOutput>(
@@ -9,7 +9,8 @@
9
9
  * (platform problem) for the runner harness to surface upstream.
10
10
  */
11
11
 
12
- import type { WorkflowHooks, WorkflowCtx } from "../types/workflow.js";
12
+ import type { WorkflowHooks } from "../types/workflow.js";
13
+ import type { InvokeChild } from "../types/execution-context.js";
13
14
  import { makeLocalSandboxProvider } from "../sandbox.js";
14
15
  import { formatError } from "../utils/errors.js";
15
16
  import { RequestContext } from "../request-context/request-context.js";
@@ -94,7 +95,7 @@ export interface RunWorkflowOptions {
94
95
  /** Provider-specific child workflow invocation. Temporal/Inngest providers
95
96
  * inject their native child-workflow primitive; the LocalProvider injects
96
97
  * the public Agent Compose API client. */
97
- invokeChild?: WorkflowCtx["invokeChild"];
98
+ invokeChild?: InvokeChild;
98
99
  }
99
100
 
100
101
  export async function runWorkflow<TInput, TOutput>(