@agent-compose/sdk 0.2.2 → 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 +1968 -746
  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 +1919 -742
  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 +213 -19
  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
@@ -17,24 +17,1423 @@ var __toESM = (mod, isNodeMode, target) => {
17
17
  };
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
+ // src/runtimes/vercel.ts
21
+ import { streamText, stepCountIs, tool } from "ai";
22
+
20
23
  // src/types/runtime.ts
21
24
  function defineRuntime(pkg) {
22
25
  if (typeof pkg.create !== "function")
23
26
  throw new Error("defineRuntime: 'create' is required");
24
27
  return pkg;
25
28
  }
26
- // src/types/workflow.ts
27
- function defineWorkflow(def) {
28
- return Object.assign(def.run, {
29
- networkPolicy: def.networkPolicy,
30
- placeholders: def.placeholders,
31
- snapshot: def.snapshot,
32
- saveSnapshot: def.saveSnapshot
29
+
30
+ // src/tools/coding.ts
31
+ import { z } from "zod";
32
+ import { isAbsolute, join } from "node:path/posix";
33
+ function q(value) {
34
+ return JSON.stringify(value);
35
+ }
36
+ function resolvePath(path, cwd) {
37
+ return cwd && !isAbsolute(path) ? join(cwd, path) : path;
38
+ }
39
+ async function readFile(sandbox, path, opts) {
40
+ const script = `
41
+ const fs = require("fs");
42
+ const path = process.argv[1];
43
+ const offset = process.argv[2] ? Number(process.argv[2]) : 1;
44
+ const limit = process.argv[3] ? Number(process.argv[3]) : 2000;
45
+ const data = fs.readFileSync(path, "utf8");
46
+ const lines = data.split(/\\r?\\n/);
47
+ const start = Math.max(1, offset);
48
+ const end = Math.min(lines.length, start + Math.max(0, limit) - 1);
49
+ for (let i = start; i <= end; i++) console.log(String(i) + ": " + lines[i - 1]);
50
+ `;
51
+ const args = [q(path), opts?.offset !== undefined ? String(opts.offset) : "", opts?.limit !== undefined ? String(opts.limit) : ""].filter(Boolean).map(q).join(" ");
52
+ const { stdout } = await sandbox.commands.run(`node -e ${q(script)} ${args}`, { cwd: opts?.cwd, timeoutMs: 30000 });
53
+ return stdout;
54
+ }
55
+ async function readRawFile(sandbox, path, cwd) {
56
+ const script = `const fs = require("fs"); process.stdout.write(fs.readFileSync(process.argv[1], "utf8"));`;
57
+ const { stdout } = await sandbox.commands.run(`node -e ${q(script)} ${q(path)}`, { cwd, timeoutMs: 30000 });
58
+ return stdout;
59
+ }
60
+ var readTool = {
61
+ name: "Read",
62
+ description: "Read a UTF-8 text file from the sandbox. Returns numbered lines. Use offset/limit for large files.",
63
+ inputSchema: z.object({
64
+ path: z.string().describe("File path to read. Relative paths resolve from the agent working directory."),
65
+ offset: z.number().int().positive().optional().describe("1-based line number to start at."),
66
+ limit: z.number().int().positive().max(5000).optional().describe("Maximum number of lines to return.")
67
+ }),
68
+ execute: ({ path, offset, limit }, ctx) => readFile(ctx.sandbox, path, { offset, limit, cwd: ctx.cwd })
69
+ };
70
+ var writeTool = {
71
+ name: "Write",
72
+ description: "Write UTF-8 content to a file in the sandbox, creating or replacing the file.",
73
+ inputSchema: z.object({
74
+ path: z.string().describe("File path to write. Relative paths resolve from the agent working directory."),
75
+ content: z.string().describe("Complete file contents to write.")
76
+ }),
77
+ execute: async ({ path, content }, ctx) => {
78
+ const target = resolvePath(path, ctx.cwd);
79
+ await ctx.sandbox.files.write(target, content);
80
+ return `Wrote ${content.length} bytes to ${target}`;
81
+ }
82
+ };
83
+ var editTool = {
84
+ name: "Edit",
85
+ description: "Edit a text file by replacing an exact string. Fails if the old string is not found or is ambiguous without replaceAll.",
86
+ inputSchema: z.object({
87
+ path: z.string().describe("File path to edit. Relative paths resolve from the agent working directory."),
88
+ oldString: z.string().min(1).describe("Exact text to replace."),
89
+ newString: z.string().describe("Replacement text."),
90
+ replaceAll: z.boolean().optional().describe("Replace every occurrence instead of requiring exactly one match.")
91
+ }),
92
+ execute: async ({ path, oldString, newString, replaceAll }, ctx) => {
93
+ const target = resolvePath(path, ctx.cwd);
94
+ const before = await readRawFile(ctx.sandbox, target, ctx.cwd);
95
+ const count = before.split(oldString).length - 1;
96
+ if (count === 0)
97
+ throw new Error(`Edit failed: oldString not found in ${target}`);
98
+ if (!replaceAll && count !== 1)
99
+ throw new Error(`Edit failed: oldString occurs ${count} times in ${target}; set replaceAll=true or make oldString more specific`);
100
+ const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString);
101
+ await ctx.sandbox.files.write(target, after);
102
+ return `Edited ${target}: replaced ${replaceAll ? count : 1} occurrence${(replaceAll ? count : 1) === 1 ? "" : "s"}`;
103
+ }
104
+ };
105
+ var bashTool = {
106
+ name: "Bash",
107
+ description: "Run a shell command in the sandbox. Use for file discovery/search (e.g. globbing/grep via shell commands), tests, builds, and package commands.",
108
+ inputSchema: z.object({
109
+ command: z.string().min(1).describe("Shell command to execute."),
110
+ timeoutMs: z.number().int().positive().max(3600000).optional().describe("Timeout in milliseconds."),
111
+ cwd: z.string().optional().describe("Optional command working directory. Defaults to the agent working directory.")
112
+ }),
113
+ execute: async ({ command, timeoutMs, cwd }, ctx) => {
114
+ const result = await ctx.sandbox.commands.run(command, {
115
+ cwd: cwd ?? ctx.cwd,
116
+ timeoutMs: timeoutMs ?? 120000
117
+ });
118
+ if (result.exitCode !== 0)
119
+ throw new Error(`Command failed with exit code ${result.exitCode}${result.stdout ? `
120
+ ${result.stdout}` : ""}`);
121
+ return result.stdout;
122
+ }
123
+ };
124
+ var codingTools = [readTool, writeTool, editTool, bashTool];
125
+ // src/processors/runner.ts
126
+ async function runProcessorChain(processors, selectHook, initial, ctx) {
127
+ let current = initial;
128
+ for (const p of processors) {
129
+ const fn = selectHook(p);
130
+ if (!fn)
131
+ continue;
132
+ const verdict = await fn.call(p, current, ctx);
133
+ if (verdict.kind !== "continue")
134
+ return verdict;
135
+ current = verdict.value;
136
+ }
137
+ return { kind: "continue", value: current };
138
+ }
139
+
140
+ // src/request-context/request-context.ts
141
+ var AC_RESERVED_PREFIX = "ac__";
142
+ var AC_TEAM_ID = "ac__teamId";
143
+ var AC_RUN_ID = "ac__runId";
144
+ var AC_WORKFLOW_ID = "ac__workflowId";
145
+ var AC_FACTORY_ID = "ac__factoryId";
146
+ var AC_API_KEY_SCOPES = "ac__apiKeyScopes";
147
+ var AC_PARENT_RUN_ID = "ac__parentRunId";
148
+ var AC_ABORT_SIGNAL = "ac__abortSignal";
149
+ var RESERVED_KEYS = new Set([
150
+ AC_TEAM_ID,
151
+ AC_RUN_ID,
152
+ AC_WORKFLOW_ID,
153
+ AC_FACTORY_ID,
154
+ AC_API_KEY_SCOPES,
155
+ AC_PARENT_RUN_ID,
156
+ AC_ABORT_SIGNAL
157
+ ]);
158
+ var SERIALISABLE_RESERVED_KEYS = new Set([
159
+ AC_TEAM_ID,
160
+ AC_RUN_ID,
161
+ AC_WORKFLOW_ID,
162
+ AC_FACTORY_ID,
163
+ AC_API_KEY_SCOPES,
164
+ AC_PARENT_RUN_ID
165
+ ]);
166
+
167
+ class ReservedKeyError extends Error {
168
+ key;
169
+ constructor(key) {
170
+ super(`request-context: "${key}" is reserved and cannot be mutated downstream`);
171
+ this.name = "ReservedKeyError";
172
+ this.key = key;
173
+ }
174
+ }
175
+
176
+ class NonSerialisableValueError extends Error {
177
+ constructor(key, cause) {
178
+ super(`request-context: value for "${key}" is not JSON-serialisable`, { cause });
179
+ this.name = "NonSerialisableValueError";
180
+ }
181
+ }
182
+ function assertJsonSerialisable(key, value, seen = new WeakSet) {
183
+ if (value === null)
184
+ return;
185
+ const type = typeof value;
186
+ if (type === "string" || type === "boolean")
187
+ return;
188
+ if (type === "number") {
189
+ if (Number.isFinite(value))
190
+ return;
191
+ throw new NonSerialisableValueError(key, new TypeError("non-finite numbers are not JSON-serialisable"));
192
+ }
193
+ if (type !== "object") {
194
+ throw new NonSerialisableValueError(key, new TypeError(`${type} is not JSON-serialisable`));
195
+ }
196
+ const objectValue = value;
197
+ if (seen.has(objectValue)) {
198
+ throw new NonSerialisableValueError(key, new TypeError("cyclic values are not JSON-serialisable"));
199
+ }
200
+ seen.add(objectValue);
201
+ if (Array.isArray(value)) {
202
+ for (const item of value)
203
+ assertJsonSerialisable(key, item, seen);
204
+ return;
205
+ }
206
+ if (Object.getPrototypeOf(objectValue) !== Object.prototype) {
207
+ throw new NonSerialisableValueError(key, new TypeError("only plain objects are JSON-serialisable"));
208
+ }
209
+ for (const item of Object.values(objectValue))
210
+ assertJsonSerialisable(key, item, seen);
211
+ }
212
+
213
+ class RequestContext {
214
+ reserved;
215
+ user;
216
+ constructor(reserved, user) {
217
+ this.reserved = Object.freeze({
218
+ ...reserved,
219
+ apiKeyScopes: Object.freeze([...reserved.apiKeyScopes])
220
+ });
221
+ this.user = user;
222
+ }
223
+ static fromReserved(reserved) {
224
+ return new RequestContext(reserved, new Map);
225
+ }
226
+ static deserialise(wire) {
227
+ const ctx = new RequestContext({ ...wire.reserved, abortSignal: undefined }, new Map(Object.entries(wire.user)));
228
+ return ctx;
229
+ }
230
+ withAbortSignal(signal) {
231
+ return new RequestContext({ ...this.reserved, abortSignal: signal }, new Map(this.user));
232
+ }
233
+ get teamId() {
234
+ return this.reserved.teamId;
235
+ }
236
+ get runId() {
237
+ return this.reserved.runId;
238
+ }
239
+ get workflowId() {
240
+ return this.reserved.workflowId;
241
+ }
242
+ get factoryId() {
243
+ return this.reserved.factoryId;
244
+ }
245
+ get apiKeyScopes() {
246
+ return this.reserved.apiKeyScopes;
247
+ }
248
+ get parentRunId() {
249
+ return this.reserved.parentRunId;
250
+ }
251
+ get abortSignal() {
252
+ return this.reserved.abortSignal;
253
+ }
254
+ hasScope(scope) {
255
+ return this.reserved.apiKeyScopes.includes(scope);
256
+ }
257
+ set(key, value) {
258
+ if (RESERVED_KEYS.has(key) || key.startsWith(AC_RESERVED_PREFIX)) {
259
+ throw new ReservedKeyError(key);
260
+ }
261
+ assertJsonSerialisable(key, value);
262
+ this.user.set(key, value);
263
+ }
264
+ get(key) {
265
+ return this.user.get(key);
266
+ }
267
+ has(key) {
268
+ return this.user.has(key);
269
+ }
270
+ delete(key) {
271
+ if (RESERVED_KEYS.has(key) || key.startsWith(AC_RESERVED_PREFIX)) {
272
+ throw new ReservedKeyError(key);
273
+ }
274
+ return this.user.delete(key);
275
+ }
276
+ entries() {
277
+ return this.user.entries();
278
+ }
279
+ serialise() {
280
+ return {
281
+ reserved: {
282
+ teamId: this.reserved.teamId,
283
+ runId: this.reserved.runId,
284
+ workflowId: this.reserved.workflowId,
285
+ factoryId: this.reserved.factoryId,
286
+ apiKeyScopes: [...this.reserved.apiKeyScopes],
287
+ parentRunId: this.reserved.parentRunId
288
+ },
289
+ user: Object.fromEntries(this.user)
290
+ };
291
+ }
292
+ forChild(opts) {
293
+ return RequestContext.fromReserved({
294
+ teamId: this.reserved.teamId,
295
+ runId: opts.runId,
296
+ workflowId: opts.workflowId,
297
+ factoryId: this.reserved.factoryId,
298
+ apiKeyScopes: this.reserved.apiKeyScopes,
299
+ parentRunId: this.reserved.runId
300
+ });
301
+ }
302
+ }
303
+
304
+ // src/utils/errors.ts
305
+ function formatError(err) {
306
+ return err instanceof Error ? err.message : String(err);
307
+ }
308
+
309
+ // src/runtimes/vercel.ts
310
+ function now() {
311
+ return new Date().toISOString();
312
+ }
313
+ function serialiseOutput(value) {
314
+ return typeof value === "string" ? value : JSON.stringify(value ?? "");
315
+ }
316
+ function toAgentMessages(part) {
317
+ const ts = now();
318
+ switch (part.type) {
319
+ case "text-delta":
320
+ case "text":
321
+ return [{ type: "text", text: String(part.text ?? ""), timestamp: ts }];
322
+ case "reasoning-delta":
323
+ case "reasoning":
324
+ return [{ type: "thinking", text: String(part.text ?? ""), timestamp: ts }];
325
+ case "tool-call":
326
+ return [{
327
+ type: "tool_use",
328
+ toolName: String(part.toolName ?? ""),
329
+ toolInput: part.input ?? {},
330
+ toolUseId: String(part.toolCallId ?? ""),
331
+ timestamp: ts
332
+ }];
333
+ case "tool-result":
334
+ return [{
335
+ type: "tool_result",
336
+ toolUseId: String(part.toolCallId ?? ""),
337
+ output: serialiseOutput(part.output),
338
+ isError: false,
339
+ timestamp: ts
340
+ }];
341
+ case "tool-error":
342
+ return [{
343
+ type: "tool_result",
344
+ toolUseId: String(part.toolCallId ?? ""),
345
+ output: formatError(part.error),
346
+ isError: true,
347
+ timestamp: ts
348
+ }];
349
+ case "error":
350
+ return [{ type: "error", text: formatError(part.error), timestamp: ts }];
351
+ default:
352
+ return [];
353
+ }
354
+ }
355
+
356
+ class VercelRunner {
357
+ sandbox;
358
+ options;
359
+ config;
360
+ supportsToolCallProcessor = true;
361
+ tools;
362
+ messages = [];
363
+ constructor(sandbox, options, config) {
364
+ this.sandbox = sandbox;
365
+ this.options = options;
366
+ this.config = config;
367
+ this.tools = config.tools ?? codingTools;
368
+ }
369
+ async gateToolCall(call, ctx) {
370
+ const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
371
+ if (verdict.kind === "continue")
372
+ return { kind: "allow", call: verdict.value };
373
+ return verdict;
374
+ }
375
+ buildTools(iteration, signal) {
376
+ const set = {};
377
+ for (const t of this.tools) {
378
+ if (this.options.allowedTools && !this.options.allowedTools.includes(t.name))
379
+ continue;
380
+ set[t.name] = tool({
381
+ description: t.description,
382
+ inputSchema: t.inputSchema,
383
+ execute: async (input, execOpts) => {
384
+ const call = {
385
+ toolName: t.name,
386
+ toolInput: input,
387
+ toolUseId: execOpts.toolCallId ?? `${t.name}-${Date.now()}`
388
+ };
389
+ const requestContext = this.options.requestContext ?? RequestContext.fromReserved({
390
+ teamId: "",
391
+ runId: "",
392
+ workflowId: "",
393
+ factoryId: null,
394
+ apiKeyScopes: [],
395
+ parentRunId: null
396
+ });
397
+ const gate = await this.gateToolCall(call, {
398
+ requestContext,
399
+ abortSignal: execOpts.abortSignal ?? signal ?? new AbortController().signal,
400
+ retryCount: 0,
401
+ agentId: this.options.agentId ?? "agent",
402
+ iteration
403
+ });
404
+ if (gate.kind === "deny")
405
+ return gate.reason;
406
+ if (gate.kind === "abort")
407
+ throw new Error(gate.reason);
408
+ return t.execute(gate.call.toolInput, {
409
+ sandbox: this.sandbox,
410
+ cwd: this.options.cwd,
411
+ abortSignal: execOpts.abortSignal ?? signal
412
+ });
413
+ }
414
+ });
415
+ }
416
+ return set;
417
+ }
418
+ async* sendMessage(opts) {
419
+ const sessionId = opts.sessionId ?? `vercel-${Date.now()}`;
420
+ yield { type: "init", sessionId, timestamp: now() };
421
+ const inputMessages = [...this.messages, { role: "user", content: opts.prompt }];
422
+ const result = streamText({
423
+ model: this.config.model,
424
+ ...this.config.system ? { system: this.config.system } : {},
425
+ messages: inputMessages,
426
+ tools: this.buildTools(opts.iteration ?? 1, opts.signal),
427
+ stopWhen: stepCountIs(this.options.maxTurns ?? 40),
428
+ abortSignal: opts.signal
429
+ });
430
+ try {
431
+ for await (const part of result.fullStream) {
432
+ for (const msg of toAgentMessages(part))
433
+ yield msg;
434
+ }
435
+ const response = await result.response;
436
+ const responseMessages = response.messages;
437
+ if (responseMessages) {
438
+ this.messages.length = 0;
439
+ this.messages.push(...responseMessages);
440
+ }
441
+ let usage;
442
+ try {
443
+ usage = await result.usage;
444
+ } catch {
445
+ usage = undefined;
446
+ }
447
+ if (usage) {
448
+ yield {
449
+ type: "usage",
450
+ inputTokens: usage.inputTokens ?? 0,
451
+ outputTokens: usage.outputTokens ?? 0,
452
+ cacheReadTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
453
+ cacheCreationTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
454
+ durationMs: 0,
455
+ numTurns: opts.iteration ?? 1,
456
+ timestamp: now()
457
+ };
458
+ }
459
+ } catch (err) {
460
+ yield { type: "error", text: formatError(err), timestamp: now() };
461
+ return;
462
+ }
463
+ yield { type: "done", sessionId, timestamp: now() };
464
+ }
465
+ }
466
+ function createVercelRuntime(config) {
467
+ return defineRuntime({
468
+ create: (sandbox, opts) => new VercelRunner(sandbox, opts, config)
469
+ });
470
+ }
471
+ // src/types/workflow.ts
472
+ import { z as z2 } from "zod";
473
+
474
+ // src/workflow-steps/types.ts
475
+ var WORKFLOW_BRAND = Symbol.for("@agent-compose/sdk.Workflow");
476
+
477
+ // src/types/workflow-metadata.ts
478
+ function extractMetadata(source) {
479
+ const out = {};
480
+ if (source.networkPolicy !== undefined)
481
+ out.networkPolicy = freezeMetadataValue(source.networkPolicy);
482
+ if (source.placeholders !== undefined)
483
+ out.placeholders = Object.freeze({ ...source.placeholders });
484
+ if (source.snapshot !== undefined)
485
+ out.snapshot = source.snapshot;
486
+ if (source.saveSnapshot !== undefined)
487
+ out.saveSnapshot = source.saveSnapshot;
488
+ if (source.processors !== undefined)
489
+ out.processors = Object.freeze([...source.processors]);
490
+ if (source.memory !== undefined)
491
+ out.memory = typeof source.memory === "object" && source.memory !== null ? Object.freeze({ ...source.memory }) : source.memory;
492
+ return Object.freeze(out);
493
+ }
494
+ function freezeMetadataValue(value) {
495
+ if (value === null || typeof value !== "object")
496
+ return value;
497
+ if (Array.isArray(value)) {
498
+ for (const v of value)
499
+ freezeMetadataValue(v);
500
+ return Object.freeze(value);
501
+ }
502
+ for (const v of Object.values(value)) {
503
+ freezeMetadataValue(v);
504
+ }
505
+ return Object.freeze(value);
506
+ }
507
+
508
+ // src/workflow-steps/workflow.ts
509
+ function createStepWorkflow(opts) {
510
+ if (!opts.id)
511
+ throw new Error("defineWorkflow: 'id' is required for step-based workflows");
512
+ const build = (steps) => ({
513
+ step(step) {
514
+ return build([...steps, step]);
515
+ },
516
+ build() {
517
+ if (steps.length === 0) {
518
+ throw new Error(`defineWorkflow("${opts.id}").build: no steps registered — add at least one with .step(step)`);
519
+ }
520
+ const finalStep = steps[steps.length - 1];
521
+ if (finalStep.output !== opts.output) {
522
+ throw new Error(`defineWorkflow("${opts.id}").build: final step "${finalStep.name}" output schema is not the same Zod schema instance as the workflow's declared output schema. ` + `Pass the same schema reference to both the final step and defineWorkflow.`);
523
+ }
524
+ const workflow = {
525
+ id: opts.id,
526
+ input: opts.input,
527
+ output: opts.output,
528
+ steps: Object.freeze([...steps]),
529
+ metadata: extractMetadata(opts)
530
+ };
531
+ Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
532
+ return Object.freeze(workflow);
533
+ }
534
+ });
535
+ return build([]);
536
+ }
537
+ function isWorkflow(value) {
538
+ return typeof value === "object" && value !== null && value[WORKFLOW_BRAND] === true;
539
+ }
540
+
541
+ // src/types/workflow.ts
542
+ function compileRunForm(def, metadata) {
543
+ const unknownSchema = z2.unknown();
544
+ const step = {
545
+ name: "run",
546
+ input: unknownSchema,
547
+ output: unknownSchema,
548
+ run: async (stepCtx) => {
549
+ const workflowCtx = {
550
+ input: stepCtx.input,
551
+ run: stepCtx.run,
552
+ requestContext: stepCtx.requestContext,
553
+ invokeChild: stepCtx.invokeChild,
554
+ setMetadata: stepCtx.setMetadata,
555
+ step: stepCtx.step,
556
+ agentEvents: stepCtx.agentEvents,
557
+ processors: metadata.processors ?? []
558
+ };
559
+ const sandbox = stepCtx.sandbox;
560
+ if (!sandbox)
561
+ throw new Error("legacy run-form workflow requires a sandbox in StepContext");
562
+ return def.run(workflowCtx, sandbox);
563
+ }
564
+ };
565
+ const workflow = {
566
+ id: "@run-form",
567
+ input: unknownSchema,
568
+ output: unknownSchema,
569
+ steps: Object.freeze([step]),
570
+ metadata
571
+ };
572
+ Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
573
+ return Object.freeze(workflow);
574
+ }
575
+ function defineWorkflow(def) {
576
+ if ("run" in def)
577
+ return compileRunForm(def, extractMetadata(def));
578
+ return createStepWorkflow(def);
579
+ }
580
+ // src/types/sandbox-environment.ts
581
+ function defineSandboxEnvironment(env) {
582
+ if (typeof env.setup !== "function") {
583
+ throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
584
+ }
585
+ return defineWorkflow({
586
+ saveSnapshot: env.saveSnapshot ?? true,
587
+ run: async (_ctx, sandbox) => env.setup(sandbox)
588
+ });
589
+ }
590
+ // src/processors/processor.ts
591
+ var Verdict = {
592
+ continue: (value) => ({ kind: "continue", value }),
593
+ deny: (reason) => ({ kind: "deny", reason }),
594
+ abort: (reason) => ({ kind: "abort", reason })
595
+ };
596
+ // src/processors/builtins.ts
597
+ function denyTools(names) {
598
+ const banned = new Set(names);
599
+ return {
600
+ name: "denyTools",
601
+ processToolCall: (call) => banned.has(call.toolName) ? Verdict.deny(`tool "${call.toolName}" is denied by policy`) : Verdict.continue(call)
602
+ };
603
+ }
604
+ function requireScope(required) {
605
+ return {
606
+ name: "requireScope",
607
+ processInput: (prompt, ctx) => {
608
+ const missing = required.filter((s) => !ctx.requestContext.hasScope(s));
609
+ return missing.length === 0 ? Verdict.continue(prompt) : Verdict.abort(`API key is missing required scopes: ${missing.join(", ")}`);
610
+ }
611
+ };
612
+ }
613
+ function redactPattern(pattern, replacement = "[REDACTED]") {
614
+ return {
615
+ name: "redactPattern",
616
+ processOutput: (message) => {
617
+ if (message.type === "text" || message.type === "thinking") {
618
+ const redacted = message.text.replace(pattern, replacement);
619
+ if (redacted !== message.text) {
620
+ return Verdict.continue({ ...message, text: redacted });
621
+ }
622
+ }
623
+ return Verdict.continue(message);
624
+ }
625
+ };
626
+ }
627
+ // src/client.ts
628
+ import { ofetch } from "ofetch";
629
+
630
+ // src/errors.ts
631
+ class AgentComposeError extends Error {
632
+ status;
633
+ constructor(status, message) {
634
+ super(message);
635
+ this.status = status;
636
+ this.name = "AgentComposeError";
637
+ }
638
+ }
639
+
640
+ // src/sse.ts
641
+ async function* parseSseStream(body) {
642
+ const reader = body.getReader();
643
+ const decoder = new TextDecoder;
644
+ let buffer = "";
645
+ let event = "";
646
+ let seq = 0;
647
+ let dataLines = [];
648
+ try {
649
+ while (true) {
650
+ const { done, value } = await reader.read();
651
+ if (done)
652
+ break;
653
+ buffer += decoder.decode(value, { stream: true });
654
+ const lines = buffer.split(`
655
+ `);
656
+ buffer = lines.pop() ?? "";
657
+ for (const line of lines) {
658
+ if (line.startsWith("id:")) {
659
+ seq = Number(line.slice(3).trim());
660
+ continue;
661
+ }
662
+ if (line.startsWith("event:")) {
663
+ event = line.slice(6).trim();
664
+ continue;
665
+ }
666
+ if (line.startsWith("data:")) {
667
+ dataLines.push(line.slice(5).trim());
668
+ continue;
669
+ }
670
+ if (line === "" && dataLines.length > 0) {
671
+ try {
672
+ const data = JSON.parse(dataLines.join(`
673
+ `));
674
+ yield { id: seq, event, data };
675
+ } catch {}
676
+ event = "";
677
+ seq = 0;
678
+ dataLines = [];
679
+ }
680
+ }
681
+ }
682
+ } finally {
683
+ reader.releaseLock();
684
+ }
685
+ }
686
+
687
+ // src/client.ts
688
+ var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
689
+ var DEFAULT_FACTORY = "default";
690
+ function detectAmbientParentRunId() {
691
+ const envRunId = typeof process !== "undefined" ? process.env?.RUN_ID : undefined;
692
+ return envRunId && UUID_REGEX.test(envRunId) ? envRunId : null;
693
+ }
694
+ function templatePath(factorySlug, ...rest) {
695
+ const tail = rest.length > 0 ? "/" + rest.map(encodeURIComponent).join("/") : "";
696
+ return `/api/v1/factories/${encodeURIComponent(factorySlug)}/templates${tail}`;
697
+ }
698
+
699
+ class AgentComposeClient {
700
+ fetch;
701
+ baseUrl;
702
+ apiKey;
703
+ constructor(baseUrl, apiKey) {
704
+ this.baseUrl = baseUrl.replace(/\/$/, "");
705
+ this.apiKey = apiKey;
706
+ this.fetch = ofetch.create({
707
+ baseURL: this.baseUrl,
708
+ headers: { Authorization: `Bearer ${apiKey}` },
709
+ async onResponseError({ response }) {
710
+ const body = response._data;
711
+ throw new AgentComposeError(response.status, body?.error ?? response.statusText);
712
+ }
713
+ });
714
+ }
715
+ register(payload) {
716
+ const { factorySlug = DEFAULT_FACTORY, ...body } = payload;
717
+ return this.fetch(templatePath(factorySlug), { method: "POST", body });
718
+ }
719
+ invoke(name, input, opts) {
720
+ const parentRunId = opts?.parentRunId === undefined ? detectAmbientParentRunId() : opts.parentRunId;
721
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
722
+ return this.fetch(templatePath(factorySlug, name, "invoke"), {
723
+ method: "POST",
724
+ body: {
725
+ input,
726
+ ...opts?.snapshot !== undefined ? { snapshot: opts.snapshot } : {},
727
+ ...opts?.saveSnapshot !== undefined ? { saveSnapshot: opts.saveSnapshot } : {},
728
+ ...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
729
+ ...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {},
730
+ ...parentRunId ? { parentRunId } : {},
731
+ ...opts?.agentId ? { agentId: opts.agentId } : {}
732
+ }
733
+ });
734
+ }
735
+ async invokeAndWait(name, input, opts) {
736
+ const timeoutMs = opts?.timeoutMs ?? 30 * 60 * 1000;
737
+ const pollMs = opts?.pollIntervalMs ?? 1000;
738
+ const { id: runId } = await this.invoke(name, input, opts);
739
+ const deadline = Date.now() + timeoutMs;
740
+ while (Date.now() < deadline) {
741
+ const status = await this.getStatus(runId);
742
+ if (status.status === "success" || status.status === "failed" || status.status === "abandoned") {
743
+ return status;
744
+ }
745
+ await new Promise((r) => setTimeout(r, pollMs));
746
+ }
747
+ throw new AgentComposeError(504, `invokeAndWait: run ${runId} did not settle within ${timeoutMs}ms`);
748
+ }
749
+ async listSnapshots(opts) {
750
+ const q2 = new URLSearchParams;
751
+ if (opts?.workflow)
752
+ q2.set("workflow", opts.workflow);
753
+ if (opts?.limit != null)
754
+ q2.set("limit", String(opts.limit));
755
+ const body = await this.fetch(`/api/v1/snapshots${q2.toString() ? `?${q2}` : ""}`);
756
+ return body.data;
757
+ }
758
+ deleteSnapshot(runId) {
759
+ return this.fetch(`/api/v1/workflows/${runId}/snapshot`, { method: "DELETE" });
760
+ }
761
+ getStatus(runId) {
762
+ return this.fetch(`/api/v1/workflows/${runId}/status`);
763
+ }
764
+ getRun(runId) {
765
+ return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}`);
766
+ }
767
+ async getRunTimeline(runId, opts) {
768
+ const q2 = new URLSearchParams;
769
+ if (opts?.limit !== undefined)
770
+ q2.set("limit", String(opts.limit));
771
+ if (opts?.offset !== undefined)
772
+ q2.set("offset", String(opts.offset));
773
+ const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/timeline${q2.toString() ? `?${q2}` : ""}`);
774
+ return body.events;
775
+ }
776
+ async reportEvent(runId, input) {
777
+ const body = {
778
+ ...input,
779
+ ...input.timestamp instanceof Date ? { timestamp: input.timestamp.toISOString() } : {}
780
+ };
781
+ const response = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/events`, {
782
+ method: "POST",
783
+ body
784
+ });
785
+ return response.event;
786
+ }
787
+ async listRunEvents(runId) {
788
+ const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/events`);
789
+ return body.events;
790
+ }
791
+ async listFactoryEvents(opts) {
792
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
793
+ const q2 = new URLSearchParams;
794
+ if (opts?.limit != null)
795
+ q2.set("limit", String(opts.limit));
796
+ if (opts?.name)
797
+ q2.set("name", opts.name);
798
+ if (opts?.from)
799
+ q2.set("from", opts.from);
800
+ if (opts?.before)
801
+ q2.set("before", opts.before);
802
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/events${q2.toString() ? `?${q2}` : ""}`);
803
+ }
804
+ async listRunLogs(runId, opts) {
805
+ const q2 = new URLSearchParams;
806
+ if (opts?.afterId !== undefined)
807
+ q2.set("afterId", String(opts.afterId));
808
+ if (opts?.limit !== undefined)
809
+ q2.set("limit", String(opts.limit));
810
+ if (opts?.direction !== undefined)
811
+ q2.set("direction", opts.direction);
812
+ const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/logs${q2.toString() ? `?${q2}` : ""}`);
813
+ return body.logs;
814
+ }
815
+ async listTemplates(opts) {
816
+ const path = opts?.factorySlug ? templatePath(opts.factorySlug) : "/api/v1/templates";
817
+ const body = await this.fetch(path);
818
+ return body.templates;
819
+ }
820
+ async listFactories() {
821
+ const body = await this.fetch("/api/v1/factories");
822
+ return body.factories;
823
+ }
824
+ createFactory(payload) {
825
+ return this.fetch("/api/v1/factories", { method: "POST", body: payload });
826
+ }
827
+ getFactory(slug) {
828
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`);
829
+ }
830
+ updateFactory(slug, updates) {
831
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "PATCH", body: updates });
832
+ }
833
+ deleteFactory(slug) {
834
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "DELETE" });
835
+ }
836
+ setSecret(workflowName, key, value, opts) {
837
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
838
+ return this.fetch(templatePath(factorySlug, workflowName, "secrets"), {
839
+ method: "POST",
840
+ body: { key, value }
841
+ });
842
+ }
843
+ async listSecrets(workflowName, opts) {
844
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
845
+ const body = await this.fetch(templatePath(factorySlug, workflowName, "secrets"));
846
+ return body.secrets.map((s) => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
847
+ }
848
+ deleteSecret(workflowName, key, opts) {
849
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
850
+ return this.fetch(templatePath(factorySlug, workflowName, "secrets", key), { method: "DELETE" });
851
+ }
852
+ createApiKey(input) {
853
+ return this.fetch("/api-keys", { method: "POST", body: input });
854
+ }
855
+ async listApiKeys() {
856
+ const body = await this.fetch("/api-keys");
857
+ return body.data;
858
+ }
859
+ getUsage(from, to) {
860
+ const qs = `?from=${encodeURIComponent(from.toISOString())}&to=${encodeURIComponent(to.toISOString())}`;
861
+ return this.fetch(`/api/v1/usage${qs}`);
862
+ }
863
+ cancelRun(runId) {
864
+ return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
865
+ }
866
+ async* streamRunLogs(runId, opts) {
867
+ const headers = { Authorization: `Bearer ${this.apiKey}` };
868
+ if (opts?.lastEventId && opts.lastEventId > 0) {
869
+ headers["Last-Event-ID"] = String(opts.lastEventId);
870
+ }
871
+ const res = await fetch(`${this.baseUrl}/api/v1/workflows/${encodeURIComponent(runId)}/stream`, {
872
+ headers,
873
+ ...opts?.signal ? { signal: opts.signal } : {}
874
+ });
875
+ if (!res.ok || !res.body) {
876
+ let message = res.statusText;
877
+ try {
878
+ const body = await res.json();
879
+ if (body.error)
880
+ message = body.error;
881
+ } catch {}
882
+ throw new AgentComposeError(res.status, message);
883
+ }
884
+ for await (const ev of parseSseStream(res.body)) {
885
+ yield ev.data;
886
+ }
887
+ }
888
+ }
889
+ // src/utils/discovery.ts
890
+ function discoverRuntimeName(source) {
891
+ return source.match(/runtime:\s*["']([^"']+)["']/)?.[1] ?? null;
892
+ }
893
+ // src/utils/bundler.ts
894
+ import { createHash } from "node:crypto";
895
+ import { parse as babelParse } from "@babel/parser";
896
+
897
+ // src/utils/source-loader.ts
898
+ import { dirname } from "path";
899
+ import { writeFile, rm, mkdir } from "node:fs/promises";
900
+ var TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
901
+ var LATEST_VERSION = "$LATEST";
902
+ async function importSourceModule(source, tmpPath) {
903
+ await mkdir(dirname(tmpPath), { recursive: true });
904
+ await writeFile(tmpPath, source, "utf8");
905
+ const mod = await import(tmpPath);
906
+ const cleanup = () => rm(tmpPath, { force: true });
907
+ return { mod, cleanup };
908
+ }
909
+
910
+ // src/types/workflow-plan.ts
911
+ function workflowPlan(steps) {
912
+ return { steps };
913
+ }
914
+
915
+ // src/utils/bundler.ts
916
+ var BUNDLER_VERSION = 1;
917
+
918
+ class WorkflowSourceValidationError extends Error {
919
+ constructor(message) {
920
+ super(message);
921
+ this.name = "WorkflowSourceValidationError";
922
+ }
923
+ }
924
+ async function bundle(path, label) {
925
+ const bun = globalThis;
926
+ if (!bun.Bun?.build)
927
+ throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
928
+ const result = await bun.Bun.build({ entrypoints: [path], format: "esm", target: "node" });
929
+ if (!result.success) {
930
+ throw new Error(`Failed to bundle ${label}:
931
+ ${result.logs.map((l) => l.message).join(`
932
+ `)}`);
933
+ }
934
+ return result.outputs[0].text();
935
+ }
936
+ async function extractFromBundle(source, label, pick) {
937
+ const tmpPath = `/tmp/agentc-${label}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`;
938
+ try {
939
+ const loaded = await importSourceModule(source, tmpPath);
940
+ try {
941
+ return pick(loaded.mod.default);
942
+ } finally {
943
+ await loaded.cleanup();
944
+ }
945
+ } catch {
946
+ return;
947
+ }
948
+ }
949
+ var DEFINE_WORKFLOW_CALLEE = /^defineWorkflow\d*$/;
950
+ function isDefineWorkflowCall(node) {
951
+ if (node.callee.type === "Identifier") {
952
+ return DEFINE_WORKFLOW_CALLEE.test(node.callee.name);
953
+ }
954
+ if (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") {
955
+ return isDefineWorkflowCall(node.callee.object);
956
+ }
957
+ return false;
958
+ }
959
+ function assertDefaultExportIsDefineWorkflow(source, label) {
960
+ let ast;
961
+ try {
962
+ ast = babelParse(source, {
963
+ sourceType: "module",
964
+ plugins: [],
965
+ errorRecovery: false
966
+ });
967
+ } catch (err) {
968
+ const msg = err instanceof Error ? err.message : String(err);
969
+ throw new WorkflowSourceValidationError(`Bundled workflow ${label} failed to parse as JavaScript: ${msg}. ` + `If you registered raw .ts source, switch to \`agentc register <file.ts>\` so the source is bundled first.`);
970
+ }
971
+ const defaultExpr = findDefaultExportExpression(ast.program.body);
972
+ if (!defaultExpr) {
973
+ throw new WorkflowSourceValidationError(`Bundled workflow ${label} has no default export. ` + `Use \`export default defineWorkflow({ run: ... })\`.`);
974
+ }
975
+ if (defaultExpr.type !== "CallExpression" || !isDefineWorkflowCall(defaultExpr)) {
976
+ throw new WorkflowSourceValidationError(`Bundled workflow ${label} default export must be a \`defineWorkflow({...})\` call ` + `(or a \`defineWorkflow(...).step(...).build()\` chain). ` + `Got: ${defaultExpr.type}. Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk".`);
977
+ }
978
+ }
979
+ function findDefaultExportExpression(body) {
980
+ const defaultDecl = body.find((node) => node.type === "ExportDefaultDeclaration");
981
+ if (defaultDecl) {
982
+ return defaultDecl.declaration;
983
+ }
984
+ const exportNamed = body.find((node) => node.type === "ExportNamedDeclaration" && node.specifiers.some((spec2) => spec2.type === "ExportSpecifier" && spec2.exported.type === "Identifier" && spec2.exported.name === "default"));
985
+ if (!exportNamed || exportNamed.type !== "ExportNamedDeclaration")
986
+ return null;
987
+ const spec = exportNamed.specifiers.find((s) => s.type === "ExportSpecifier" && s.exported.type === "Identifier" && s.exported.name === "default");
988
+ if (!spec || spec.type !== "ExportSpecifier" || spec.local.type !== "Identifier")
989
+ return null;
990
+ const localName = spec.local.name;
991
+ for (const stmt of body) {
992
+ if (stmt.type !== "VariableDeclaration")
993
+ continue;
994
+ for (const decl of stmt.declarations) {
995
+ if (decl.id.type === "Identifier" && decl.id.name === localName && decl.init && decl.init.type !== "TSSatisfiesExpression") {
996
+ return decl.init;
997
+ }
998
+ }
999
+ }
1000
+ return null;
1001
+ }
1002
+ function sha256(text) {
1003
+ return createHash("sha256").update(text, "utf8").digest("hex");
1004
+ }
1005
+ async function bundleWorkflow(workflowPath, overrides) {
1006
+ const label = `"${workflowPath}"`;
1007
+ const source = await bundle(workflowPath, `workflow ${label}`);
1008
+ assertDefaultExportIsDefineWorkflow(source, label);
1009
+ const workflow = await extractFromBundle(source, "workflow", (wf) => isWorkflow(wf) ? wf : undefined);
1010
+ if (!workflow) {
1011
+ throw new WorkflowSourceValidationError(`Bundled workflow ${label} default export is not a Workflow. ` + `Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk" — ` + `raw \`export default async function\` is no longer supported.`);
1012
+ }
1013
+ const { metadata } = workflow;
1014
+ const plan = workflowPlan(workflow.steps.map((step, index) => ({ index, name: step.name })));
1015
+ const manifest = {
1016
+ definitionBrand: true,
1017
+ sourceHash: sha256(source),
1018
+ bundlerVersion: BUNDLER_VERSION
1019
+ };
1020
+ return {
1021
+ source,
1022
+ manifest,
1023
+ workflowPlan: plan,
1024
+ networkPolicy: overrides?.networkPolicy ?? metadata.networkPolicy,
1025
+ placeholders: overrides?.placeholders ?? metadata.placeholders,
1026
+ ...metadata.snapshot !== undefined ? { snapshot: metadata.snapshot } : {},
1027
+ ...metadata.saveSnapshot !== undefined ? { saveSnapshot: metadata.saveSnapshot } : {},
1028
+ ...metadata.memory !== undefined ? { memory: metadata.memory } : {}
1029
+ };
1030
+ }
1031
+ // src/utils/schemas.ts
1032
+ import { z as z3 } from "zod";
1033
+ var AgentStatusSchema = z3.object({
1034
+ summary: z3.string(),
1035
+ completed: z3.array(z3.string()),
1036
+ blockers: z3.array(z3.string()),
1037
+ exit_signal: z3.boolean()
1038
+ });
1039
+ // src/runtimes/claude.ts
1040
+ import { query } from "@anthropic-ai/claude-agent-sdk";
1041
+
1042
+ // src/agent/agent-loop.ts
1043
+ import { z as z5 } from "zod";
1044
+
1045
+ // src/agent/protocol.ts
1046
+ import { z as z4 } from "zod";
1047
+ var AgentMessageSchema = z4.object({
1048
+ type: z4.enum(["init", "text", "thinking", "tool_use", "tool_result", "done", "error", "usage"]),
1049
+ timestamp: z4.string()
1050
+ }).passthrough();
1051
+ function parseAgentResponse(text) {
1052
+ const match = text.match(/<response>([\s\S]*?)<\/response>/);
1053
+ if (!match)
1054
+ return null;
1055
+ try {
1056
+ return JSON.parse(match[1].trim());
1057
+ } catch {
1058
+ return null;
1059
+ }
1060
+ }
1061
+
1062
+ // src/agent/agent-loop.ts
1063
+ import { randomUUID } from "node:crypto";
1064
+ var DEFAULT_CLAUDE_MODEL = "claude-opus-4-7";
1065
+ var SAME_BLOCKER_ITERATIONS = 3;
1066
+ var STALL_ITERATIONS = 3;
1067
+ var MESSAGE_PREVIEW_CHARS = 400;
1068
+ function parseAgentStatus(text) {
1069
+ const match = text.match(/<status>([\s\S]*?)<\/status>/);
1070
+ if (!match)
1071
+ return null;
1072
+ try {
1073
+ const result = AgentStatusSchema.safeParse(JSON.parse(match[1].trim()));
1074
+ return result.success ? result.data : null;
1075
+ } catch {
1076
+ return null;
1077
+ }
1078
+ }
1079
+ var DEFAULT_ALLOWED_TOOLS = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch"];
1080
+ function truncate(value) {
1081
+ return value.length > MESSAGE_PREVIEW_CHARS ? `${value.slice(0, MESSAGE_PREVIEW_CHARS)}…` : value;
1082
+ }
1083
+ function preview(value) {
1084
+ try {
1085
+ return truncate(JSON.stringify(value));
1086
+ } catch {
1087
+ return truncate(String(value));
1088
+ }
1089
+ }
1090
+ function summarizeAgentMessage(msg) {
1091
+ switch (msg.type) {
1092
+ case "init":
1093
+ return { type: "init", sessionId: msg.sessionId };
1094
+ case "text":
1095
+ return { type: "text", text: msg.text };
1096
+ case "thinking":
1097
+ return { type: "thinking", text: msg.text };
1098
+ case "tool_use":
1099
+ return { type: "tool_use", toolName: msg.toolName, toolUseId: msg.toolUseId, toolInputPreview: preview(msg.toolInput) };
1100
+ case "tool_result":
1101
+ return { type: "tool_result", toolUseId: msg.toolUseId, output: truncate(msg.output), isError: msg.isError };
1102
+ case "usage":
1103
+ return {
1104
+ type: "usage",
1105
+ inputTokens: msg.inputTokens,
1106
+ outputTokens: msg.outputTokens,
1107
+ cacheReadTokens: msg.cacheReadTokens,
1108
+ cacheCreationTokens: msg.cacheCreationTokens,
1109
+ durationMs: msg.durationMs,
1110
+ numTurns: msg.numTurns
1111
+ };
1112
+ case "done":
1113
+ return { type: "done", sessionId: msg.sessionId };
1114
+ case "error":
1115
+ return { type: "error", text: truncate(msg.text) };
1116
+ }
1117
+ }
1118
+ async function agentLoop(opts) {
1119
+ const agentId = opts.agentId ?? randomUUID();
1120
+ const label = opts.label ?? "agent";
1121
+ const logLabel = opts.label ?? "[Agent Loop]";
1122
+ const startedAt = Date.now();
1123
+ const turnsPerIteration = opts.turnsPerIteration ?? 40;
1124
+ const maxIterations = opts.maxIterations ?? 8;
1125
+ const processors = opts.processors ?? [];
1126
+ const requestContext = opts.requestContext ?? RequestContext.fromReserved({
1127
+ teamId: "",
1128
+ runId: "",
1129
+ workflowId: "",
1130
+ factoryId: null,
1131
+ apiKeyScopes: [],
1132
+ parentRunId: null
1133
+ });
1134
+ const loopAbort = new AbortController;
1135
+ const buildProcCtx = (iteration) => ({
1136
+ requestContext,
1137
+ abortSignal: loopAbort.signal,
1138
+ retryCount: 0,
1139
+ agentId,
1140
+ iteration
1141
+ });
1142
+ if (!opts.runtime)
1143
+ throw new Error("agentLoop: opts.runtime is required");
1144
+ const client = opts.runtime({
1145
+ maxTurns: turnsPerIteration,
1146
+ allowedTools: opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS,
1147
+ label: logLabel,
1148
+ cwd: opts.cwd,
1149
+ processors,
1150
+ requestContext,
1151
+ agentId,
1152
+ ...opts.responseSchema ? { outputFormat: { type: "json_schema", schema: z5.toJSONSchema(opts.responseSchema) } } : {}
1153
+ });
1154
+ if (processors.some((p) => p.processToolCall) && !client.supportsToolCallProcessor) {
1155
+ process.stderr.write(`${logLabel} processToolCall registered but runtime does not support pre-tool-use gating — hook is dormant
1156
+ `);
1157
+ }
1158
+ let lastSessionId = "";
1159
+ let lastStatus = null;
1160
+ let iterationsWithoutStatus = 0;
1161
+ let blockerStreak = null;
1162
+ let completedIterations = 0;
1163
+ let lastResponseText = "";
1164
+ let lastResponseValidationError = "";
1165
+ opts.onAgentLifecycleEvent?.({ event: "agent.spawned", at: startedAt, agentId, label, allowedTools: opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS });
1166
+ try {
1167
+ for (let iteration = 0;iteration < maxIterations; iteration++) {
1168
+ const procCtx = buildProcCtx(iteration + 1);
1169
+ const initialPrompt = opts.buildPrompt(lastStatus, iteration);
1170
+ const inputVerdict = await runProcessorChain(processors, (p) => p.processInput, initialPrompt, procCtx);
1171
+ if (inputVerdict.kind !== "continue") {
1172
+ loopAbort.abort();
1173
+ throw new Error(`${logLabel} input ${inputVerdict.kind === "deny" ? "rejected" : "aborted"}: ${inputVerdict.reason}`);
1174
+ }
1175
+ const prompt = inputVerdict.value;
1176
+ process.stdout.write(`${logLabel} iteration ${iteration + 1}/${maxIterations} · ${turnsPerIteration} turns
1177
+ `);
1178
+ let responseText = "";
1179
+ for await (const rawMsg of client.sendMessage({ prompt, sessionId: iteration > 0 ? lastSessionId : undefined, iteration: iteration + 1 })) {
1180
+ const outputVerdict = await runProcessorChain(processors, (p) => p.processOutput, rawMsg, procCtx);
1181
+ if (outputVerdict.kind === "abort") {
1182
+ loopAbort.abort();
1183
+ throw new Error(`${logLabel} output aborted: ${outputVerdict.reason}`);
1184
+ }
1185
+ if (outputVerdict.kind === "deny") {
1186
+ process.stderr.write(`${logLabel} message dropped by processor: ${outputVerdict.reason}
1187
+ `);
1188
+ continue;
1189
+ }
1190
+ const msg = outputVerdict.value;
1191
+ opts.onAgentEvent?.(iteration, msg);
1192
+ opts.onAgentLifecycleEvent?.({ event: "agent.message", at: Date.now(), agentId, label, iteration: iteration + 1, message: summarizeAgentMessage(msg) });
1193
+ if (msg.type === "init")
1194
+ lastSessionId = msg.sessionId;
1195
+ if (msg.type === "text")
1196
+ responseText += msg.text;
1197
+ if (msg.type === "error")
1198
+ throw new Error(`Agent error: ${msg.text}`);
1199
+ }
1200
+ lastResponseText = responseText;
1201
+ let status = parseAgentStatus(responseText);
1202
+ const rawResponse = opts.responseSchema ? parseAgentResponse(responseText) ?? parseRawJsonResponse(responseText) : null;
1203
+ if (opts.responseSchema && rawResponse !== null) {
1204
+ const parsed = opts.responseSchema.safeParse(rawResponse);
1205
+ if (parsed.success) {
1206
+ const successStatus = status ?? { summary: "structured response completed", completed: [], blockers: [], exit_signal: true };
1207
+ process.stdout.write(`${logLabel} structured response validated after ${iteration + 1}/${maxIterations} iterations
1208
+ `);
1209
+ opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "success", iterations: iteration + 1, durationMs: Date.now() - startedAt });
1210
+ return { agentId, label, sessionId: lastSessionId, lastStatus: successStatus, iterations: iteration + 1, response: parsed.data };
1211
+ }
1212
+ lastResponseValidationError = parsed.error.message;
1213
+ }
1214
+ completedIterations = iteration + 1;
1215
+ process.stdout.write(`${logLabel} iteration ${iteration + 1} status: exit_signal=${status?.exit_signal ?? "(no status)"} blockers=${JSON.stringify(status?.blockers ?? [])}
1216
+ `);
1217
+ lastStatus = status ?? lastStatus;
1218
+ opts.onIteration?.(iteration + 1, status);
1219
+ opts.onAgentLifecycleEvent?.({ event: "agent.iteration", at: Date.now(), agentId, label, iteration: iteration + 1, status });
1220
+ if (status?.exit_signal && (status.blockers?.length ?? 0) === 0) {
1221
+ let response = status;
1222
+ if (opts.responseSchema) {
1223
+ if (rawResponse === null) {
1224
+ process.stderr.write(`${logLabel} NO <response> BLOCK — response tail: ${responseText.slice(-400)}
1225
+ `);
1226
+ status = { ...status, exit_signal: false, blockers: ["No <response> block found — emit a <response> block with the required JSON fields before setting exit_signal: true"] };
1227
+ opts.onIteration?.(iteration + 1, status);
1228
+ continue;
1229
+ }
1230
+ const parsed = opts.responseSchema.safeParse({ ...status, ...rawResponse });
1231
+ if (!parsed.success) {
1232
+ lastResponseValidationError = parsed.error.message;
1233
+ process.stderr.write(`${logLabel} <response> SCHEMA FAILED: ${parsed.error.message}
1234
+ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
1235
+ `);
1236
+ status = { ...status, exit_signal: false, blockers: [`<response> schema validation failed: ${parsed.error.message}`] };
1237
+ opts.onIteration?.(iteration + 1, status);
1238
+ continue;
1239
+ }
1240
+ response = parsed.data;
1241
+ }
1242
+ process.stdout.write(`${logLabel} done after ${iteration + 1}/${maxIterations} iterations
1243
+ `);
1244
+ opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "success", iterations: iteration + 1, durationMs: Date.now() - startedAt });
1245
+ return { agentId, label, sessionId: lastSessionId, lastStatus: status, iterations: iteration + 1, response };
1246
+ }
1247
+ if (!status) {
1248
+ if (++iterationsWithoutStatus >= STALL_ITERATIONS)
1249
+ throw new Error(`${logLabel} stalled: no <status> block after ${iterationsWithoutStatus} iterations`);
1250
+ } else {
1251
+ iterationsWithoutStatus = 0;
1252
+ }
1253
+ if (status?.blockers?.length) {
1254
+ const key = status.blockers.join("|");
1255
+ if (blockerStreak !== null && blockerStreak.key === key) {
1256
+ if (++blockerStreak.count >= SAME_BLOCKER_ITERATIONS)
1257
+ throw new Error(`${logLabel} circuit break: same blocker repeated ${blockerStreak.count}x — "${status.blockers[0]}"`);
1258
+ } else {
1259
+ blockerStreak = { key, count: 1 };
1260
+ }
1261
+ } else {
1262
+ blockerStreak = null;
1263
+ }
1264
+ if (iteration + 1 < maxIterations)
1265
+ process.stdout.write(`${logLabel} continuing to iteration ${iteration + 2}/${maxIterations}
1266
+ `);
1267
+ }
1268
+ if (opts.responseSchema)
1269
+ throw new Error(`${logLabel} did not produce a valid <response> after ${maxIterations} iterations${lastResponseValidationError ? `: ${lastResponseValidationError}` : ""}. Response tail: ${lastResponseText.slice(-600)}`);
1270
+ process.stderr.write(`${logLabel} exhausted ${maxIterations} iterations, proceeding with available work
1271
+ `);
1272
+ opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "success", iterations: maxIterations, durationMs: Date.now() - startedAt });
1273
+ return { agentId, label, sessionId: lastSessionId, lastStatus, iterations: maxIterations };
1274
+ } catch (err) {
1275
+ opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "failed", iterations: completedIterations, durationMs: Date.now() - startedAt, failureReason: err instanceof Error ? err.message : String(err) });
1276
+ throw err;
1277
+ }
1278
+ }
1279
+ function parseRawJsonResponse(text) {
1280
+ const trimmed = text.trim();
1281
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
1282
+ return null;
1283
+ try {
1284
+ return JSON.parse(trimmed);
1285
+ } catch {
1286
+ return null;
1287
+ }
1288
+ }
1289
+
1290
+ // src/runtimes/claude.ts
1291
+ function now2() {
1292
+ return new Date().toISOString();
1293
+ }
1294
+ function translateMessage(message) {
1295
+ const ts = now2();
1296
+ const msgs = [];
1297
+ if (message.type === "system" && message.subtype === "init") {
1298
+ msgs.push({ type: "init", sessionId: String(message.session_id ?? ""), timestamp: ts });
1299
+ return msgs;
1300
+ }
1301
+ if (message.type === "assistant") {
1302
+ const raw = message.message;
1303
+ for (const block of raw?.content ?? []) {
1304
+ const b = block;
1305
+ if (b.type === "text")
1306
+ msgs.push({ type: "text", text: String(b.text ?? ""), timestamp: ts });
1307
+ if (b.type === "thinking")
1308
+ msgs.push({ type: "thinking", text: String(b.thinking ?? ""), timestamp: ts });
1309
+ if (b.type === "tool_use")
1310
+ msgs.push({ type: "tool_use", toolName: String(b.name ?? ""), toolInput: b.input ?? {}, toolUseId: String(b.id ?? ""), timestamp: ts });
1311
+ }
1312
+ return msgs;
1313
+ }
1314
+ if (message.type === "user") {
1315
+ const raw = message.message;
1316
+ for (const block of raw?.content ?? []) {
1317
+ const b = block;
1318
+ if (b.type === "tool_result")
1319
+ msgs.push({ type: "tool_result", toolUseId: String(b.tool_use_id ?? ""), output: typeof b.content === "string" ? b.content : JSON.stringify(b.content ?? ""), isError: Boolean(b.is_error), timestamp: ts });
1320
+ }
1321
+ return msgs;
1322
+ }
1323
+ if (message.type === "result") {
1324
+ if (typeof message.result === "string" && message.result.trim()) {
1325
+ msgs.push({ type: "text", text: message.result, timestamp: ts });
1326
+ }
1327
+ const usage = message.usage;
1328
+ if (usage)
1329
+ msgs.push({
1330
+ type: "usage",
1331
+ inputTokens: usage.input_tokens ?? 0,
1332
+ outputTokens: usage.output_tokens ?? 0,
1333
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
1334
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
1335
+ durationMs: Number(message.duration_ms ?? 0),
1336
+ numTurns: Number(message.num_turns ?? 0),
1337
+ timestamp: ts
1338
+ });
1339
+ if (message.is_error || message.subtype === "error_during_execution") {
1340
+ msgs.push({ type: "error", text: formatError(message.error ?? message.result ?? message.message), timestamp: ts });
1341
+ } else {
1342
+ msgs.push({ type: "done", sessionId: String(message.session_id ?? ""), timestamp: ts });
1343
+ }
1344
+ }
1345
+ return msgs;
1346
+ }
1347
+
1348
+ class ClaudeRunner {
1349
+ options;
1350
+ config;
1351
+ supportsToolCallProcessor = true;
1352
+ constructor(_sandbox, options = {}, config = {}) {
1353
+ this.options = options;
1354
+ this.config = config;
1355
+ }
1356
+ async gateToolCall(call, ctx) {
1357
+ const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
1358
+ if (verdict.kind === "continue")
1359
+ return { kind: "allow", call: verdict.value };
1360
+ return verdict;
1361
+ }
1362
+ async* sendMessage(opts) {
1363
+ const requestContext = this.options.requestContext ?? RequestContext.fromReserved({
1364
+ teamId: "",
1365
+ runId: "",
1366
+ workflowId: "",
1367
+ factoryId: null,
1368
+ apiKeyScopes: [],
1369
+ parentRunId: null
1370
+ });
1371
+ const preToolHook = async (input, toolUseId, { signal }) => {
1372
+ const pre = input;
1373
+ const call = {
1374
+ toolName: pre.tool_name,
1375
+ toolInput: pre.tool_input ?? {},
1376
+ toolUseId: toolUseId ?? `${pre.tool_name}-${Date.now()}`
1377
+ };
1378
+ const gate = await this.gateToolCall(call, {
1379
+ requestContext,
1380
+ abortSignal: signal ?? opts.signal ?? new AbortController().signal,
1381
+ retryCount: 0,
1382
+ agentId: this.options.agentId ?? "agent",
1383
+ iteration: opts.iteration ?? 1
1384
+ });
1385
+ if (gate.kind === "allow")
1386
+ return { hookSpecificOutput: { hookEventName: pre.hook_event_name, permissionDecision: "allow", updatedInput: gate.call.toolInput } };
1387
+ if (gate.kind === "deny")
1388
+ return { hookSpecificOutput: { hookEventName: pre.hook_event_name, permissionDecision: "deny", permissionDecisionReason: gate.reason } };
1389
+ return { continue: false, systemMessage: gate.reason };
1390
+ };
1391
+ try {
1392
+ let emittedAssistantText = false;
1393
+ for await (const message of query({
1394
+ prompt: opts.prompt,
1395
+ options: {
1396
+ tools: this.options.allowedTools,
1397
+ allowedTools: this.options.allowedTools,
1398
+ maxTurns: this.options.maxTurns,
1399
+ model: this.config.model ?? this.options.model ?? DEFAULT_CLAUDE_MODEL,
1400
+ outputFormat: this.options.outputFormat,
1401
+ thinking: this.config.thinking,
1402
+ effort: this.config.effort,
1403
+ cwd: this.options.cwd,
1404
+ env: { ...process.env, ...this.config.env ?? {} },
1405
+ pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? "claude",
1406
+ ...this.config.claudeMdContent ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.config.claudeMdContent } } : {},
1407
+ resume: opts.sessionId,
1408
+ mcpServers: this.config.mcpServers,
1409
+ permissionMode: "acceptEdits",
1410
+ hooks: { PreToolUse: [{ hooks: [preToolHook] }] }
1411
+ }
1412
+ })) {
1413
+ const raw = message;
1414
+ const translated = translateMessage(raw);
1415
+ for (const msg of translated) {
1416
+ if (raw.type === "result" && msg.type === "text" && emittedAssistantText)
1417
+ continue;
1418
+ if (msg.type === "text" && raw.type === "assistant")
1419
+ emittedAssistantText = true;
1420
+ yield msg;
1421
+ }
1422
+ }
1423
+ } catch (err) {
1424
+ yield { type: "error", text: formatError(err), timestamp: now2() };
1425
+ }
1426
+ }
1427
+ }
1428
+ function createClaudeRuntime(config = {}) {
1429
+ return defineRuntime({
1430
+ create: (sandbox, opts) => new ClaudeRunner(sandbox, opts, config)
33
1431
  });
34
1432
  }
1433
+ var claude_default = createClaudeRuntime();
35
1434
  // src/sandbox.ts
36
1435
  import { promises as fs } from "node:fs";
37
- import { dirname } from "node:path";
1436
+ import { dirname as dirname2 } from "node:path";
38
1437
  import { spawn } from "node:child_process";
39
1438
  import { Sandbox } from "e2b";
40
1439
  import { Sandbox as Desktop } from "@e2b/desktop";
@@ -42,7 +1441,16 @@ import pRetry from "p-retry";
42
1441
  var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
43
1442
  var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
44
1443
  function makeSandboxProvider(sb) {
45
- return { sandboxId: sb.sandboxId, commands: sb.commands, files: sb.files, kill: () => sb.kill() };
1444
+ return {
1445
+ sandboxId: sb.sandboxId,
1446
+ commands: sb.commands,
1447
+ files: {
1448
+ async write(path, content) {
1449
+ await sb.files.write(path, content);
1450
+ }
1451
+ },
1452
+ kill: () => sb.kill()
1453
+ };
46
1454
  }
47
1455
  function makeDesktopSandboxProvider(sb) {
48
1456
  return {
@@ -105,7 +1513,7 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
105
1513
  commands: {
106
1514
  async run(cmd, opts) {
107
1515
  if (opts?.background) {
108
- sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts.cwd, env: mergeEnvs(opts.envs), detached: true }).catch(() => {});
1516
+ sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts.cwd, env: mergeEnvs(opts.envs), detached: true }).catch((err) => console.error(`[sandbox] background command failed: ${err instanceof Error ? err.message : String(err)}`));
109
1517
  return { exitCode: 0, stdout: "" };
110
1518
  }
111
1519
  const signal = opts?.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined;
@@ -168,7 +1576,7 @@ function makeLocalSandboxProvider() {
168
1576
  },
169
1577
  files: {
170
1578
  async write(path, content) {
171
- await fs.mkdir(dirname(path), { recursive: true });
1579
+ await fs.mkdir(dirname2(path), { recursive: true });
172
1580
  await fs.writeFile(path, content);
173
1581
  }
174
1582
  },
@@ -239,7 +1647,7 @@ var SANDBOX_PROVIDERS = {
239
1647
  sandboxes.push(...await paginator.nextItems());
240
1648
  if (sandboxes.length === 0)
241
1649
  return;
242
- await Promise.all(sandboxes.map((s) => Sandbox.kill(s.sandboxId).catch(() => {})));
1650
+ await Promise.all(sandboxes.map((s) => Sandbox.kill(s.sandboxId)));
243
1651
  console.info(`[sandbox] killed ${sandboxes.length} stale E2B sandboxes (tag: ${AGENT_COMPOSE_TAG})`);
244
1652
  },
245
1653
  getActiveCount: async () => (await SANDBOX_PROVIDERS.e2b.listOwned({})).length,
@@ -326,722 +1734,235 @@ async function listOwnedSandboxes() {
326
1734
  out[name] = err instanceof Error ? err : new Error(String(err));
327
1735
  }
328
1736
  }));
329
- return out;
330
- }
331
- async function killSandboxById(provider, sandboxId) {
332
- const def = SANDBOX_PROVIDERS[provider];
333
- if (!def?.reconnect)
334
- throw new Error(`Provider "${provider}" does not support reconnect (required for kill-by-id)`);
335
- const sb = await def.reconnect(sandboxId);
336
- await sb.kill();
337
- }
338
- async function killAllSandboxes(onError) {
339
- await Promise.allSettled(Object.entries(SANDBOX_PROVIDERS).filter(([, def]) => def.killAll).map(([name, def]) => {
340
- const env = Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k] ?? ""]));
341
- if (Object.keys(def.requiredEnv).some((k) => !process.env[k]))
342
- return Promise.resolve();
343
- return def.killAll(env).catch((err) => onError?.(name, err));
344
- }));
345
- }
346
-
347
- // src/types/sandbox-environment.ts
348
- function defineSandboxEnvironment(env) {
349
- if (typeof env.setup !== "function") {
350
- throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
351
- }
352
- return defineWorkflow({
353
- saveSnapshot: env.saveSnapshot ?? true,
354
- run: () => env.setup(makeLocalSandboxProvider())
355
- });
356
- }
357
- // src/client.ts
358
- import { ofetch } from "ofetch";
359
-
360
- // src/errors.ts
361
- class AgentComposeError extends Error {
362
- status;
363
- constructor(status, message) {
364
- super(message);
365
- this.status = status;
366
- this.name = "AgentComposeError";
367
- }
368
- }
369
-
370
- // src/sse.ts
371
- async function* parseSseStream(body) {
372
- const reader = body.getReader();
373
- const decoder = new TextDecoder;
374
- let buffer = "";
375
- let event = "";
376
- let seq = 0;
377
- let dataLines = [];
378
- try {
379
- while (true) {
380
- const { done, value } = await reader.read();
381
- if (done)
382
- break;
383
- buffer += decoder.decode(value, { stream: true });
384
- const lines = buffer.split(`
385
- `);
386
- buffer = lines.pop() ?? "";
387
- for (const line of lines) {
388
- if (line.startsWith("id:")) {
389
- seq = Number(line.slice(3).trim());
390
- continue;
391
- }
392
- if (line.startsWith("event:")) {
393
- event = line.slice(6).trim();
394
- continue;
395
- }
396
- if (line.startsWith("data:")) {
397
- dataLines.push(line.slice(5).trim());
398
- continue;
399
- }
400
- if (line === "" && dataLines.length > 0) {
401
- try {
402
- const data = JSON.parse(dataLines.join(`
403
- `));
404
- yield { id: seq, event, data };
405
- } catch {}
406
- event = "";
407
- seq = 0;
408
- dataLines = [];
409
- }
410
- }
411
- }
412
- } finally {
413
- reader.releaseLock();
414
- }
415
- }
416
-
417
- // src/client.ts
418
- var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
419
- var DEFAULT_FACTORY = "default";
420
- function detectAmbientParentRunId() {
421
- const envRunId = typeof process !== "undefined" ? process.env?.RUN_ID : undefined;
422
- return envRunId && UUID_REGEX.test(envRunId) ? envRunId : null;
423
- }
424
- function templatePath(factorySlug, ...rest) {
425
- const tail = rest.length > 0 ? "/" + rest.map(encodeURIComponent).join("/") : "";
426
- return `/api/v1/factories/${encodeURIComponent(factorySlug)}/templates${tail}`;
427
- }
428
-
429
- class AgentComposeClient {
430
- fetch;
431
- baseUrl;
432
- apiKey;
433
- constructor(baseUrl, apiKey) {
434
- this.baseUrl = baseUrl.replace(/\/$/, "");
435
- this.apiKey = apiKey;
436
- this.fetch = ofetch.create({
437
- baseURL: this.baseUrl,
438
- headers: { Authorization: `Bearer ${apiKey}` },
439
- async onResponseError({ response }) {
440
- const body = response._data;
441
- throw new AgentComposeError(response.status, body?.error ?? response.statusText);
442
- }
443
- });
444
- }
445
- register(payload) {
446
- const { factorySlug = DEFAULT_FACTORY, ...body } = payload;
447
- return this.fetch(templatePath(factorySlug), { method: "POST", body });
448
- }
449
- invoke(name, input, opts) {
450
- const parentRunId = opts?.parentRunId === undefined ? detectAmbientParentRunId() : opts.parentRunId;
451
- const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
452
- return this.fetch(templatePath(factorySlug, name, "invoke"), {
453
- method: "POST",
454
- body: {
455
- input,
456
- ...opts?.snapshot !== undefined ? { snapshot: opts.snapshot } : {},
457
- ...opts?.saveSnapshot !== undefined ? { saveSnapshot: opts.saveSnapshot } : {},
458
- ...parentRunId ? { parentRunId } : {},
459
- ...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
460
- ...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}
461
- }
462
- });
463
- }
464
- async invokeAndWait(name, input, opts) {
465
- const timeoutMs = opts?.timeoutMs ?? 30 * 60 * 1000;
466
- const pollMs = opts?.pollIntervalMs ?? 1000;
467
- const { id: runId } = await this.invoke(name, input, {
468
- ...opts?.snapshot !== undefined ? { snapshot: opts.snapshot } : {},
469
- ...opts?.saveSnapshot !== undefined ? { saveSnapshot: opts.saveSnapshot } : {},
470
- ...opts?.parentRunId !== undefined ? { parentRunId: opts.parentRunId } : {},
471
- ...opts?.factorySlug !== undefined ? { factorySlug: opts.factorySlug } : {},
472
- ...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
473
- ...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}
474
- });
475
- const deadline = Date.now() + timeoutMs;
476
- while (Date.now() < deadline) {
477
- const status = await this.getStatus(runId);
478
- if (status.status === "success" || status.status === "failed" || status.status === "abandoned") {
479
- return status;
480
- }
481
- await new Promise((r) => setTimeout(r, pollMs));
482
- }
483
- throw new AgentComposeError(504, `invokeAndWait: run ${runId} did not settle within ${timeoutMs}ms`);
484
- }
485
- async listSnapshots(opts) {
486
- const q = new URLSearchParams;
487
- if (opts?.workflow)
488
- q.set("workflow", opts.workflow);
489
- if (opts?.limit != null)
490
- q.set("limit", String(opts.limit));
491
- const body = await this.fetch(`/api/v1/snapshots${q.toString() ? `?${q}` : ""}`);
492
- return body.data;
493
- }
494
- deleteSnapshot(runId) {
495
- return this.fetch(`/api/v1/workflows/${runId}/snapshot`, { method: "DELETE" });
496
- }
497
- getStatus(runId) {
498
- return this.fetch(`/api/v1/workflows/${runId}/status`);
499
- }
500
- async listTemplates(opts) {
501
- const path = opts?.factorySlug ? templatePath(opts.factorySlug) : "/api/v1/templates";
502
- const body = await this.fetch(path);
503
- return body.templates;
504
- }
505
- async listFactories() {
506
- const body = await this.fetch("/api/v1/factories");
507
- return body.factories;
508
- }
509
- createFactory(payload) {
510
- return this.fetch("/api/v1/factories", { method: "POST", body: payload });
511
- }
512
- getFactory(slug) {
513
- return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`);
514
- }
515
- updateFactory(slug, updates) {
516
- return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "PATCH", body: updates });
517
- }
518
- deleteFactory(slug) {
519
- return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "DELETE" });
520
- }
521
- setSecret(workflowName, key, value, opts) {
522
- const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
523
- return this.fetch(templatePath(factorySlug, workflowName, "secrets"), {
524
- method: "POST",
525
- body: { key, value }
526
- });
527
- }
528
- async listSecrets(workflowName, opts) {
529
- const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
530
- const body = await this.fetch(templatePath(factorySlug, workflowName, "secrets"));
531
- return body.secrets.map((s) => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
532
- }
533
- deleteSecret(workflowName, key, opts) {
534
- const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
535
- return this.fetch(templatePath(factorySlug, workflowName, "secrets", key), { method: "DELETE" });
536
- }
537
- createApiKey(input) {
538
- return this.fetch("/api-keys", { method: "POST", body: input });
539
- }
540
- async listApiKeys() {
541
- const body = await this.fetch("/api-keys");
542
- return body.data;
543
- }
544
- getUsage(from, to) {
545
- const qs = `?from=${encodeURIComponent(from.toISOString())}&to=${encodeURIComponent(to.toISOString())}`;
546
- return this.fetch(`/api/v1/usage${qs}`);
547
- }
548
- cancelRun(runId) {
549
- return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
550
- }
551
- async* streamRunLogs(runId, opts) {
552
- const headers = { Authorization: `Bearer ${this.apiKey}` };
553
- if (opts?.lastEventId && opts.lastEventId > 0) {
554
- headers["Last-Event-ID"] = String(opts.lastEventId);
555
- }
556
- const res = await fetch(`${this.baseUrl}/api/v1/workflows/${encodeURIComponent(runId)}/stream`, {
557
- headers,
558
- ...opts?.signal ? { signal: opts.signal } : {}
559
- });
560
- if (!res.ok || !res.body) {
561
- let message = res.statusText;
562
- try {
563
- const body = await res.json();
564
- if (body.error)
565
- message = body.error;
566
- } catch {}
567
- throw new AgentComposeError(res.status, message);
568
- }
569
- for await (const ev of parseSseStream(res.body)) {
570
- yield ev.data;
571
- }
572
- }
573
- }
574
- // src/utils/errors.ts
575
- function formatError(err) {
576
- return err instanceof Error ? err.message : String(err);
577
- }
578
- // src/utils/discovery.ts
579
- function discoverRuntimeName(source) {
580
- return source.match(/runtime:\s*["']([^"']+)["']/)?.[1] ?? null;
1737
+ return out;
581
1738
  }
582
- // src/utils/source-loader.ts
583
- import { dirname as dirname2 } from "path";
584
- import { writeFile, unlink, mkdir } from "node:fs/promises";
585
- var TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
586
- var LATEST_VERSION = "$LATEST";
587
- async function importSourceModule(source, tmpPath) {
588
- await mkdir(dirname2(tmpPath), { recursive: true });
589
- await writeFile(tmpPath, source, "utf8");
590
- const mod = await import(tmpPath);
591
- const cleanup = () => unlink(tmpPath).catch(() => {});
592
- return { mod, cleanup };
1739
+ async function killSandboxById(provider, sandboxId) {
1740
+ const def = SANDBOX_PROVIDERS[provider];
1741
+ if (!def?.reconnect)
1742
+ throw new Error(`Provider "${provider}" does not support reconnect (required for kill-by-id)`);
1743
+ const sb = await def.reconnect(sandboxId);
1744
+ await sb.kill();
593
1745
  }
594
-
595
- // src/utils/bundler.ts
596
- async function bundle(path, label) {
597
- const bun = globalThis;
598
- if (!bun.Bun?.build)
599
- throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
600
- const result = await bun.Bun.build({ entrypoints: [path], format: "esm", target: "bun" });
601
- if (!result.success) {
602
- throw new Error(`Failed to bundle ${label}:
603
- ${result.logs.map((l) => l.message).join(`
604
- `)}`);
605
- }
606
- return result.outputs[0].text();
1746
+ async function killAllSandboxes(onError) {
1747
+ await Promise.allSettled(Object.entries(SANDBOX_PROVIDERS).filter(([, def]) => def.killAll).map(([name, def]) => {
1748
+ const env = Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k] ?? ""]));
1749
+ if (Object.keys(def.requiredEnv).some((k) => !process.env[k]))
1750
+ return Promise.resolve();
1751
+ return def.killAll(env).catch((err) => onError?.(name, err));
1752
+ }));
607
1753
  }
608
- async function extractFromBundle(source, label, pick) {
609
- const tmpPath = `/tmp/agentc-${label}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`;
610
- try {
611
- const loaded = await importSourceModule(source, tmpPath);
1754
+ // src/workflow-steps/observability.ts
1755
+ class StepObservabilityCollector {
1756
+ metadata = {};
1757
+ events = [];
1758
+ subSteps = [];
1759
+ setMetadata = async (data) => {
1760
+ Object.assign(this.metadata, data);
1761
+ };
1762
+ step = async (name, fn) => {
1763
+ const startedAt = Date.now();
612
1764
  try {
613
- return pick(loaded.mod.default);
614
- } finally {
615
- await loaded.cleanup();
1765
+ const result = await fn();
1766
+ this.subSteps.push({
1767
+ name,
1768
+ startedAt,
1769
+ durationMs: Date.now() - startedAt,
1770
+ status: "completed"
1771
+ });
1772
+ return result;
1773
+ } catch (err) {
1774
+ this.subSteps.push({
1775
+ name,
1776
+ startedAt,
1777
+ durationMs: Date.now() - startedAt,
1778
+ status: "failed",
1779
+ error: err instanceof Error ? err.message : String(err)
1780
+ });
1781
+ throw err;
616
1782
  }
617
- } catch {
618
- return;
619
- }
620
- }
621
- async function bundleWorkflow(workflowPath, overrides) {
622
- const source = await bundle(workflowPath, `workflow "${workflowPath}"`);
623
- const wfBits = await extractFromBundle(source, "workflow", (wf) => wf ? {
624
- networkPolicy: wf.networkPolicy,
625
- placeholders: wf.placeholders,
626
- snapshot: wf.snapshot,
627
- saveSnapshot: wf.saveSnapshot
628
- } : undefined);
629
- const { networkPolicy, placeholders, snapshot, saveSnapshot } = wfBits ?? {};
630
- return {
631
- source,
632
- networkPolicy: overrides?.networkPolicy ?? networkPolicy,
633
- placeholders: overrides?.placeholders ?? placeholders,
634
- ...snapshot !== undefined ? { snapshot } : {},
635
- ...saveSnapshot !== undefined ? { saveSnapshot } : {}
636
1783
  };
637
- }
638
- // src/utils/schemas.ts
639
- import { z } from "zod";
640
- var AgentStatusSchema = z.object({
641
- summary: z.string(),
642
- completed: z.array(z.string()),
643
- blockers: z.array(z.string()),
644
- exit_signal: z.boolean()
645
- });
646
- // src/agent/protocol.ts
647
- import { z as z2 } from "zod";
648
- var AgentMessageSchema = z2.object({
649
- type: z2.enum(["init", "text", "thinking", "tool_use", "tool_result", "done", "error", "usage"]),
650
- timestamp: z2.string()
651
- }).passthrough();
652
- function parseAgentResponse(text) {
653
- const match = text.match(/<response>([\s\S]*?)<\/response>/);
654
- if (!match)
655
- return null;
656
- try {
657
- return JSON.parse(match[1].trim());
658
- } catch {
659
- return null;
1784
+ agentEvents = {
1785
+ emit: (event) => {
1786
+ this.events.push(event);
1787
+ }
1788
+ };
1789
+ snapshot() {
1790
+ const hasMetadata = Object.keys(this.metadata).length > 0;
1791
+ const hasEvents = this.events.length > 0;
1792
+ const hasSubSteps = this.subSteps.length > 0;
1793
+ if (!hasMetadata && !hasEvents && !hasSubSteps)
1794
+ return;
1795
+ const result = {};
1796
+ if (hasMetadata)
1797
+ result.metadata = { ...this.metadata };
1798
+ if (hasEvents)
1799
+ result.events = [...this.events];
1800
+ if (hasSubSteps)
1801
+ result.subSteps = [...this.subSteps];
1802
+ return result;
660
1803
  }
661
1804
  }
662
1805
 
663
- // src/agent/agent-loop.ts
664
- var DEFAULT_CLAUDE_MODEL = "claude-opus-4-7";
665
- var SAME_BLOCKER_ITERATIONS = 3;
666
- var STALL_ITERATIONS = 3;
667
- function parseAgentStatus(text) {
668
- const match = text.match(/<status>([\s\S]*?)<\/status>/);
669
- if (!match)
670
- return null;
671
- try {
672
- const result = AgentStatusSchema.safeParse(JSON.parse(match[1].trim()));
673
- return result.success ? result.data : null;
674
- } catch {
675
- return null;
1806
+ // src/workflow-steps/runner.ts
1807
+ class StepValidationError extends Error {
1808
+ stepName;
1809
+ side;
1810
+ kind = "step-validation";
1811
+ constructor(stepName, side, cause) {
1812
+ super(`Step "${stepName}" ${side} failed schema validation: ${cause.message}`);
1813
+ this.stepName = stepName;
1814
+ this.side = side;
1815
+ this.name = "StepValidationError";
1816
+ this.cause = cause;
676
1817
  }
677
1818
  }
678
- var DEFAULT_ALLOWED_TOOLS = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch"];
679
- async function agentLoop(opts) {
680
- const label = opts.label ?? "[Agent Loop]";
681
- const turnsPerIteration = opts.turnsPerIteration ?? 40;
682
- const maxIterations = opts.maxIterations ?? 8;
683
- if (!opts.runtime)
684
- throw new Error("agentLoop: opts.runtime is required");
685
- const client = opts.runtime({
686
- maxTurns: turnsPerIteration,
687
- allowedTools: opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS,
688
- label,
689
- cwd: opts.cwd
690
- });
691
- let lastSessionId = "";
692
- let lastStatus = null;
693
- let iterationsWithoutStatus = 0;
694
- let blockerStreak = null;
695
- for (let iteration = 0;iteration < maxIterations; iteration++) {
696
- const prompt = opts.buildPrompt(lastStatus, iteration);
697
- process.stderr.write(`${label} iteration ${iteration + 1}/${maxIterations} · ${turnsPerIteration} turns
698
- `);
699
- let responseText = "";
700
- for await (const msg of client.sendMessage({ prompt, sessionId: iteration > 0 ? lastSessionId : undefined })) {
701
- opts.onAgentEvent?.(iteration, msg);
702
- if (msg.type === "init")
703
- lastSessionId = msg.sessionId;
704
- if (msg.type === "text")
705
- responseText += msg.text;
706
- if (msg.type === "error")
707
- throw new Error(`Agent error: ${msg.text}`);
708
- }
709
- process.stdout.write(`
710
- `);
711
- let status = parseAgentStatus(responseText);
712
- process.stderr.write(`
713
- ${label} iteration ${iteration + 1} status: exit_signal=${status?.exit_signal ?? "(no status)"} blockers=${JSON.stringify(status?.blockers ?? [])}
714
- `);
715
- lastStatus = status ?? lastStatus;
716
- opts.onIteration?.(iteration + 1, status);
717
- if (status?.exit_signal && (status.blockers?.length ?? 0) === 0) {
718
- let response = status;
719
- if (opts.responseSchema) {
720
- const raw = parseAgentResponse(responseText);
721
- if (raw === null) {
722
- process.stderr.write(`
723
- ${label} NO <response> BLOCK — response tail: ${responseText.slice(-400)}
724
- `);
725
- status = { ...status, exit_signal: false, blockers: ["No <response> block found — emit a <response> block with the required JSON fields before setting exit_signal: true"] };
726
- opts.onIteration?.(iteration + 1, status);
727
- continue;
728
- }
729
- const parsed = opts.responseSchema.safeParse({ ...status, ...raw });
730
- if (!parsed.success) {
731
- process.stderr.write(`
732
- ${label} <response> SCHEMA FAILED: ${parsed.error.message}
733
- raw: ${JSON.stringify(raw).slice(0, 400)}
734
- `);
735
- status = { ...status, exit_signal: false, blockers: [`<response> schema validation failed: ${parsed.error.message}`] };
736
- opts.onIteration?.(iteration + 1, status);
737
- continue;
738
- }
739
- response = parsed.data;
740
- }
741
- process.stderr.write(`${label} done after ${iteration + 1}/${maxIterations} iterations
742
- `);
743
- return { sessionId: lastSessionId, lastStatus: status, iterations: iteration + 1, response };
744
- }
745
- if (!status) {
746
- if (++iterationsWithoutStatus >= STALL_ITERATIONS)
747
- throw new Error(`${label} stalled: no <status> block after ${iterationsWithoutStatus} iterations`);
748
- } else {
749
- iterationsWithoutStatus = 0;
750
- }
751
- if (status?.blockers?.length) {
752
- const key = status.blockers.join("|");
753
- if (blockerStreak !== null && blockerStreak.key === key) {
754
- if (++blockerStreak.count >= SAME_BLOCKER_ITERATIONS)
755
- throw new Error(`${label} circuit break: same blocker repeated ${blockerStreak.count}x — "${status.blockers[0]}"`);
756
- } else {
757
- blockerStreak = { key, count: 1 };
758
- }
759
- } else {
760
- blockerStreak = null;
761
- }
762
- if (iteration + 1 < maxIterations)
763
- process.stderr.write(`${label} continuing to iteration ${iteration + 2}/${maxIterations}
764
- `);
1819
+
1820
+ class WorkflowInputValidationError extends Error {
1821
+ workflowId;
1822
+ kind = "workflow-input-validation";
1823
+ constructor(workflowId, cause) {
1824
+ super(`Workflow "${workflowId}" input failed schema validation: ${cause.message}`);
1825
+ this.workflowId = workflowId;
1826
+ this.name = "WorkflowInputValidationError";
1827
+ this.cause = cause;
765
1828
  }
766
- if (opts.responseSchema)
767
- throw new Error(`${label} did not produce a valid <response> after ${maxIterations} iterations`);
768
- process.stderr.write(`${label} exhausted ${maxIterations} iterations, proceeding with available work
769
- `);
770
- return { sessionId: lastSessionId, lastStatus, iterations: maxIterations };
771
1829
  }
772
1830
 
773
- // src/runtimes/claude.ts
774
- var NO_TIMEOUT = 0;
775
- function translateEvent(event) {
776
- const msgs = [];
777
- const ts = new Date().toISOString();
778
- if (event.type === "system" && event.subtype === "init") {
779
- msgs.push({ type: "init", sessionId: String(event.session_id ?? ""), timestamp: ts });
780
- return msgs;
781
- }
782
- if (event.type === "assistant") {
783
- const message = event.message;
784
- for (const block of message?.content ?? []) {
785
- const b = block;
786
- if (b.type === "text")
787
- msgs.push({ type: "text", text: String(b.text ?? ""), timestamp: ts });
788
- if (b.type === "thinking")
789
- msgs.push({ type: "thinking", text: String(b.thinking ?? ""), timestamp: ts });
790
- if (b.type === "tool_use")
791
- msgs.push({ type: "tool_use", toolName: String(b.name ?? ""), toolInput: b.input ?? {}, toolUseId: String(b.id ?? ""), timestamp: ts });
792
- }
793
- return msgs;
794
- }
795
- if (event.type === "user") {
796
- const message = event.message;
797
- for (const block of message?.content ?? []) {
798
- const b = block;
799
- if (b.type === "tool_result") {
800
- const raw = b.content;
801
- msgs.push({ type: "tool_result", toolUseId: String(b.tool_use_id ?? ""), output: typeof raw === "string" ? raw : JSON.stringify(raw ?? ""), isError: Boolean(b.is_error), timestamp: ts });
802
- }
803
- }
804
- return msgs;
1831
+ class WorkflowOutputValidationError extends Error {
1832
+ workflowId;
1833
+ kind = "workflow-output-validation";
1834
+ constructor(workflowId, cause) {
1835
+ super(`Workflow "${workflowId}" output failed schema validation: ${cause.message}`);
1836
+ this.workflowId = workflowId;
1837
+ this.name = "WorkflowOutputValidationError";
1838
+ this.cause = cause;
805
1839
  }
806
- if (event.type === "result") {
807
- const usage = event.usage;
808
- if (usage) {
809
- msgs.push({ type: "usage", inputTokens: usage.input_tokens ?? 0, outputTokens: usage.output_tokens ?? 0, cacheReadTokens: usage.cache_read_input_tokens ?? 0, cacheCreationTokens: usage.cache_creation_input_tokens ?? 0, durationMs: Number(event.duration_ms ?? 0), numTurns: Number(event.num_turns ?? 0), timestamp: ts });
810
- }
811
- if (event.is_error || event.subtype === "error_during_execution") {
812
- const raw = event.error ?? event.result ?? event.message;
813
- msgs.push({ type: "error", text: raw == null ? "Unknown error" : typeof raw === "object" ? JSON.stringify(raw) : String(raw), timestamp: ts });
814
- } else {
815
- msgs.push({ type: "done", sessionId: String(event.session_id ?? ""), timestamp: ts });
816
- }
817
- return msgs;
818
- }
819
- return msgs;
820
1840
  }
821
-
822
- class ClaudeRunner {
823
- sandbox;
824
- options;
825
- claudeMdContent;
826
- configEnv;
827
- configModel;
828
- configMcpServers;
829
- claudeMdWritten = false;
830
- homeDir = null;
831
- label;
832
- constructor(sandbox, options = {}, claudeMdContent = "", configEnv = {}, configModel, configMcpServers) {
833
- this.sandbox = sandbox;
834
- this.options = options;
835
- this.claudeMdContent = claudeMdContent;
836
- this.configEnv = configEnv;
837
- this.configModel = configModel;
838
- this.configMcpServers = configMcpServers;
839
- this.label = options.label ?? "[Agent]";
840
- }
841
- async getHomeDir() {
842
- if (!this.homeDir) {
843
- const { stdout } = await this.sandbox.commands.run("echo $HOME", { timeoutMs: 30000 });
844
- this.homeDir = stdout.trim() || "/home/user";
845
- }
846
- return this.homeDir;
1841
+ async function runWorkflowSingleStep(opts) {
1842
+ const step = opts.workflow.steps[opts.stepIndex];
1843
+ if (!step)
1844
+ throw new Error(`Step index ${opts.stepIndex} not found in workflow "${opts.workflow.id}"`);
1845
+ const parsedInput = step.input.safeParse(opts.input);
1846
+ if (!parsedInput.success)
1847
+ throw new StepValidationError(step.name, "input", parsedInput.error);
1848
+ const collector = new StepObservabilityCollector;
1849
+ const stepCtx = {
1850
+ input: parsedInput.data,
1851
+ requestContext: opts.requestContext,
1852
+ run: opts.run,
1853
+ ...opts.sandbox ? { sandbox: opts.sandbox } : {},
1854
+ stepName: step.name,
1855
+ abortSignal: opts.abortSignal ?? new AbortController().signal,
1856
+ invokeChild: opts.invokeChild ?? (() => {
1857
+ throw new Error("StepContext.invokeChild is not configured for this workflow engine");
1858
+ }),
1859
+ setMetadata: collector.setMetadata,
1860
+ step: collector.step,
1861
+ agentEvents: collector.agentEvents
1862
+ };
1863
+ const output = await step.run(stepCtx);
1864
+ const parsedOutput = step.output.safeParse(output);
1865
+ if (!parsedOutput.success)
1866
+ throw new StepValidationError(step.name, "output", parsedOutput.error);
1867
+ const observability = collector.snapshot();
1868
+ return observability === undefined ? { output: parsedOutput.data } : { output: parsedOutput.data, observability };
1869
+ }
1870
+ async function runWorkflowSteps(opts) {
1871
+ const { workflow, run, requestContext } = opts;
1872
+ const abortSignal = opts.abortSignal ?? new AbortController().signal;
1873
+ const invokeChild = opts.invokeChild ?? (() => {
1874
+ throw new Error("StepContext.invokeChild is not configured for this workflow engine");
1875
+ });
1876
+ const parsedInput = workflow.input.safeParse(opts.input);
1877
+ if (!parsedInput.success) {
1878
+ throw new WorkflowInputValidationError(workflow.id, parsedInput.error);
847
1879
  }
848
- async deployGlobalConfig() {
849
- if (this.claudeMdWritten)
850
- return;
851
- const home = await this.getHomeDir();
852
- await this.sandbox.commands.run(`mkdir -p "${home}/.claude" && find "${home}/.claude" -name "*.json" -not -name "CLAUDE.md" -delete 2>/dev/null || true`, { timeoutMs: 5000 });
853
- await this.sandbox.files.write(`${home}/.claude/CLAUDE.md`, this.claudeMdContent);
854
- if (Object.keys(this.configEnv).length) {
855
- const settings = JSON.stringify({ env: this.configEnv });
856
- const b64 = Buffer.from(settings).toString("base64");
857
- await this.sandbox.commands.run(`echo "${b64}" | base64 -d > "${home}/.claude/settings.json"`, { timeoutMs: 5000 });
1880
+ let current = parsedInput.data;
1881
+ const stepResults = [];
1882
+ for (let i = 0;i < workflow.steps.length; i++) {
1883
+ const step = workflow.steps[i];
1884
+ const cached = opts.getCachedOutput ? await opts.getCachedOutput(i, step.name) : undefined;
1885
+ if (cached !== undefined) {
1886
+ const parsedCached = step.output.safeParse(cached);
1887
+ if (!parsedCached.success) {
1888
+ throw new StepValidationError(step.name, "output", parsedCached.error);
1889
+ }
1890
+ current = parsedCached.data;
1891
+ stepResults.push({ name: step.name, status: "completed", output: parsedCached.data, durationMs: 0 });
1892
+ continue;
858
1893
  }
859
- const mcpServers = this.configMcpServers;
860
- if (mcpServers && Object.keys(mcpServers).length) {
861
- const b64 = Buffer.from(JSON.stringify({ mcpServers })).toString("base64");
862
- await this.sandbox.commands.run(`echo "${b64}" | base64 -d > "${home}/.claude.json"`, { timeoutMs: 5000 });
863
- process.stderr.write(`${this.label}[mcp] wrote ${Object.keys(mcpServers).join(",")} to ~/.claude.json
864
- `);
1894
+ await opts.onStepStarted?.(i, step.name);
1895
+ const parsedStepInput = step.input.safeParse(current);
1896
+ if (!parsedStepInput.success) {
1897
+ const err = new StepValidationError(step.name, "input", parsedStepInput.error);
1898
+ await opts.onStepFailed?.(i, step.name, err, 0);
1899
+ throw err;
865
1900
  }
866
- const placeholderB64 = Buffer.from("x-access-token:placeholder").toString("base64");
867
- await this.sandbox.commands.run([
868
- `git config --global core.askPass ""`,
869
- `git config --global credential.interactive false`,
870
- `git config --global credential.helper ""`,
871
- `git config --global "http.https://github.com/.extraHeader" "Authorization: Basic ${placeholderB64}"`
872
- ].join(" && "), { timeoutMs: 5000 }).catch((e) => console.warn(`${this.label}[init] git config failed (non-fatal): ${formatError(e)}`));
1901
+ const collector = new StepObservabilityCollector;
1902
+ const stepCtx = {
1903
+ input: parsedStepInput.data,
1904
+ requestContext,
1905
+ run,
1906
+ ...opts.sandbox ? { sandbox: opts.sandbox } : {},
1907
+ stepName: step.name,
1908
+ abortSignal,
1909
+ invokeChild,
1910
+ setMetadata: collector.setMetadata,
1911
+ step: collector.step,
1912
+ agentEvents: collector.agentEvents
1913
+ };
1914
+ const startedAt = Date.now();
1915
+ let output;
873
1916
  try {
874
- await this.sandbox.commands.run("rtk init --global --hook-only --auto-patch", { timeoutMs: 1e4 });
875
- } catch {
876
- console.warn("[Transport] rtk init failed (non-fatal) — token compression disabled");
877
- }
878
- this.claudeMdWritten = true;
879
- }
880
- async* _runCli(cmd, label, signal) {
881
- const queue = [];
882
- let notify = null;
883
- function enqueue(items) {
884
- queue.push(...items);
885
- notify?.();
886
- notify = null;
887
- }
888
- let lineBuffer = "";
889
- let stderrBuffer = "";
890
- const maxTurns = this.options.maxTurns ?? 40;
891
- function handleStdout(data) {
892
- lineBuffer += data;
893
- const lines = lineBuffer.split(`
894
- `);
895
- lineBuffer = lines.pop() ?? "";
896
- for (const line of lines) {
897
- const trimmed = line.trim();
898
- if (!trimmed)
899
- continue;
900
- try {
901
- const event = JSON.parse(trimmed);
902
- if (event.type === "assistant") {
903
- const content = event.message?.content ?? [];
904
- for (const block of content) {
905
- const b = block;
906
- if (b.type === "text")
907
- process.stdout.write(".");
908
- if (b.type === "tool_use")
909
- process.stderr.write(`
910
- ${label} ${b.name}(${JSON.stringify(b.input).slice(0, 80)})`);
911
- }
912
- } else if (event.type === "result") {
913
- process.stderr.write(`
914
- ${label}[result:${event.num_turns}/${maxTurns}/${event.subtype}]
915
- `);
916
- } else if (event.type === "system") {
917
- const ev = event;
918
- const mcpSrvs = ev.mcp_servers ?? [];
919
- const mcpStr = mcpSrvs.length ? ` mcp=[${mcpSrvs.map((s) => `${s.name}:${s.status}`).join(",")}]` : " mcp=[]";
920
- process.stderr.write(`
921
- ${label}[init:${ev.session_id}]${mcpStr}
922
- `);
923
- }
924
- enqueue(translateEvent(event));
925
- } catch {}
926
- }
1917
+ output = await step.run(stepCtx);
1918
+ } catch (err) {
1919
+ const wrapped = err instanceof Error ? err : new Error(String(err));
1920
+ const durationMs2 = Date.now() - startedAt;
1921
+ const observability2 = collector.snapshot();
1922
+ stepResults.push({
1923
+ name: step.name,
1924
+ status: "failed",
1925
+ error: wrapped.message,
1926
+ durationMs: durationMs2,
1927
+ ...observability2 ? { observability: observability2 } : {}
1928
+ });
1929
+ await opts.onStepFailed?.(i, step.name, wrapped, durationMs2);
1930
+ throw wrapped;
927
1931
  }
928
- process.stderr.write(`${label}[cwd:${this.options.cwd ?? "(none)"}]
929
- `);
930
- const runPromise = this.sandbox.commands.run(cmd, {
931
- cwd: this.options.cwd,
932
- timeoutMs: NO_TIMEOUT,
933
- onStdout: handleStdout,
934
- onStderr: (data) => {
935
- stderrBuffer += data;
936
- process.stderr.write(`${label}[stderr] ${data.trimEnd()}
937
- `);
938
- }
939
- });
940
- runPromise.catch((err) => {
941
- const e = err;
942
- if (e && typeof e === "object" && "exitCode" in e)
943
- process.stderr.write(`${label}[transport] command failed — exitCode=${e.exitCode} error=${JSON.stringify(e.error)} stdout_tail=${String(e.stdout ?? "").slice(-200)} stderr_tail=${String(e.stderr ?? "").slice(-200)}
944
- `);
945
- queue.push({ sentinel: "error", text: stderrBuffer.trim() ? `Agent process failed: ${formatError(err)}
946
-
947
- ${stderrBuffer.trim()}` : `Agent process failed: ${formatError(err)}` });
948
- }).finally(() => {
949
- if (lineBuffer.trim()) {
950
- try {
951
- enqueue(translateEvent(JSON.parse(lineBuffer.trim())));
952
- } catch {}
953
- }
954
- queue.push(null);
955
- notify?.();
956
- notify = null;
957
- });
958
- while (true) {
959
- while (queue.length > 0) {
960
- const item = queue.shift();
961
- if (item === null)
962
- return;
963
- if ("sentinel" in item) {
964
- yield { type: "error", text: item.text, timestamp: new Date().toISOString() };
965
- return;
966
- }
967
- if (signal?.aborted)
968
- return;
969
- const msg = item;
970
- yield msg.type === "error" && stderrBuffer.trim() && !msg.text.includes(stderrBuffer.trim()) ? { ...msg, text: `${msg.text}
971
-
972
- ${stderrBuffer.trim()}` } : msg;
973
- }
974
- await new Promise((r) => {
975
- notify = r;
1932
+ const parsedOutput = step.output.safeParse(output);
1933
+ if (!parsedOutput.success) {
1934
+ const err = new StepValidationError(step.name, "output", parsedOutput.error);
1935
+ const durationMs2 = Date.now() - startedAt;
1936
+ const observability2 = collector.snapshot();
1937
+ stepResults.push({
1938
+ name: step.name,
1939
+ status: "failed",
1940
+ error: err.message,
1941
+ durationMs: durationMs2,
1942
+ ...observability2 ? { observability: observability2 } : {}
976
1943
  });
1944
+ await opts.onStepFailed?.(i, step.name, err, durationMs2);
1945
+ throw err;
977
1946
  }
1947
+ const durationMs = Date.now() - startedAt;
1948
+ current = parsedOutput.data;
1949
+ const observability = collector.snapshot();
1950
+ stepResults.push({
1951
+ name: step.name,
1952
+ status: "completed",
1953
+ output: parsedOutput.data,
1954
+ durationMs,
1955
+ ...observability ? { observability } : {}
1956
+ });
1957
+ await opts.onStepCompleted?.(i, step.name, parsedOutput.data, durationMs);
978
1958
  }
979
- async* sendMessage(opts) {
980
- await this.deployGlobalConfig();
981
- if (opts.sessionId) {
982
- await this.sandbox.commands.run(`pkill -x claude 2>/dev/null || true`, { timeoutMs: 1e4 });
983
- }
984
- const maxTurns = this.options.maxTurns ?? 40;
985
- const model = this.configModel ?? this.options.model ?? DEFAULT_CLAUDE_MODEL;
986
- const label = this.label;
987
- const promptFile = `/tmp/agent-prompt-${Date.now()}.txt`;
988
- await this.sandbox.files.write(promptFile, opts.prompt);
989
- const hasMcp = this.configMcpServers && Object.keys(this.configMcpServers).length > 0;
990
- const allowedToolsFlag = !hasMcp && this.options.allowedTools?.length ? ["--allowedTools", this.options.allowedTools.join(",")] : [];
991
- const buildCmd = (sessionId) => [
992
- "claude",
993
- "--dangerously-skip-permissions",
994
- "--verbose",
995
- "--output-format",
996
- "stream-json",
997
- "--max-turns",
998
- String(maxTurns),
999
- "--model",
1000
- model,
1001
- ...allowedToolsFlag,
1002
- ...sessionId ? ["--resume", sessionId] : [],
1003
- "-p",
1004
- `"$(cat ${promptFile})"`
1005
- ].join(" ");
1006
- const MAX_529_RETRIES = 5;
1007
- const BACKOFF_MS = [60000, 120000, 240000, 480000, 600000];
1008
- let resumeSessionId = opts.sessionId;
1009
- for (let attempt = 0;attempt <= MAX_529_RETRIES; attempt++) {
1010
- let got529 = false;
1011
- let newSession = "";
1012
- for await (const msg of this._runCli(buildCmd(resumeSessionId), label, opts.signal)) {
1013
- if (msg.type === "init")
1014
- newSession = msg.sessionId;
1015
- if (msg.type === "error" && msg.text.includes("529")) {
1016
- got529 = true;
1017
- break;
1018
- }
1019
- yield msg;
1020
- if (msg.type === "done" || msg.type === "error")
1021
- return;
1022
- }
1023
- if (!got529)
1024
- return;
1025
- if (attempt === MAX_529_RETRIES) {
1026
- yield { type: "error", text: "Agent process failed: Repeated 529 Overloaded errors — all retries exhausted", timestamp: new Date().toISOString() };
1027
- return;
1028
- }
1029
- if (newSession)
1030
- resumeSessionId = newSession;
1031
- const wait = BACKOFF_MS[attempt];
1032
- process.stderr.write(`
1033
- ${label}[529] Overloaded — resuming in ${wait / 1000}s (attempt ${attempt + 1}/${MAX_529_RETRIES})${resumeSessionId ? " with session resume" : ""}
1034
- `);
1035
- await new Promise((r) => setTimeout(r, wait));
1036
- }
1959
+ const parsedFinal = workflow.output.safeParse(current);
1960
+ if (!parsedFinal.success) {
1961
+ throw new WorkflowOutputValidationError(workflow.id, parsedFinal.error);
1037
1962
  }
1963
+ return { output: parsedFinal.data, steps: stepResults };
1038
1964
  }
1039
- function createClaudeRuntime(config = {}) {
1040
- return defineRuntime({
1041
- create: (sandbox, opts) => new ClaudeRunner(sandbox, opts, config.claudeMdContent ?? "", config.env ?? {}, config.model, config.mcpServers)
1042
- });
1043
- }
1044
- var claude_default = createClaudeRuntime();
1965
+
1045
1966
  // src/workflows/engine.ts
1046
1967
  class WorkflowError extends Error {
1047
1968
  kind = "workflow";
@@ -1071,41 +1992,288 @@ function parseNameVersion(ref) {
1071
1992
  const atIdx = ref.lastIndexOf("@");
1072
1993
  return atIdx > 0 ? { name: ref.slice(0, atIdx), version: ref.slice(atIdx + 1) } : { name: ref, version: undefined };
1073
1994
  }
1995
+ function defaultRequestContext(runId, workflowId = "") {
1996
+ return RequestContext.fromReserved({
1997
+ teamId: "",
1998
+ runId,
1999
+ workflowId,
2000
+ factoryId: null,
2001
+ apiKeyScopes: [],
2002
+ parentRunId: null
2003
+ });
2004
+ }
1074
2005
  async function runWorkflow(wf, ctx, opts) {
1075
2006
  const hooks = opts?.hooks ?? {};
1076
- const setMetadata = opts?.setMetadata ?? (async () => {});
2007
+ const requestContext = opts?.requestContext ?? defaultRequestContext(ctx.run.id, wf.id);
1077
2008
  const sandbox = makeLocalSandboxProvider();
1078
- let workflowErr;
1079
- let wfOutput;
2009
+ const invokeChild = opts?.invokeChild ?? (() => {
2010
+ throw new Error("StepContext.invokeChild is not configured for this workflow engine");
2011
+ });
1080
2012
  try {
1081
- wfOutput = await wf({
1082
- ...ctx,
1083
- setMetadata,
1084
- step: async (name, fn) => {
1085
- const startedAt = Date.now();
2013
+ const result = await runWorkflowSteps({
2014
+ workflow: wf,
2015
+ input: ctx.input,
2016
+ run: ctx.run,
2017
+ requestContext,
2018
+ sandbox,
2019
+ invokeChild,
2020
+ ...opts?.getCachedOutput ? { getCachedOutput: opts.getCachedOutput } : {},
2021
+ onStepStarted: async (idx, name) => {
1086
2022
  hooks.onStepStart?.(name);
1087
- try {
1088
- const r = await fn();
1089
- hooks.onStepComplete?.(name, Date.now() - startedAt);
1090
- return r;
1091
- } catch (err) {
1092
- hooks.onStepFail?.(name, Date.now() - startedAt, formatError(err));
1093
- throw err;
2023
+ await opts?.onStepStarted?.(idx, name);
2024
+ },
2025
+ onStepCompleted: async (idx, name, output, durationMs) => {
2026
+ hooks.onStepComplete?.(name, durationMs);
2027
+ await opts?.onStepCompleted?.(idx, name, output, durationMs);
2028
+ },
2029
+ onStepFailed: async (idx, name, err, durationMs) => {
2030
+ hooks.onStepFail?.(name, durationMs, formatError(err));
2031
+ await opts?.onStepFailed?.(idx, name, err, durationMs);
2032
+ }
2033
+ });
2034
+ return { response: result.output ?? null };
2035
+ } catch (err) {
2036
+ if (err instanceof WorkflowError)
2037
+ throw err;
2038
+ throw new WorkflowError(formatError(err), { cause: err });
2039
+ }
2040
+ }
2041
+ // src/workflows/invoke-child.ts
2042
+ function buildInvokeChild(runId, opts = {}) {
2043
+ let childClient = null;
2044
+ const getChildClient = () => {
2045
+ if (childClient)
2046
+ return childClient;
2047
+ const baseUrl = process.env.AGENT_COMPOSE_URL ?? opts.fallbackBaseUrl;
2048
+ const apiKey = process.env.AGENT_COMPOSE_API_KEY;
2049
+ if (!baseUrl || !apiKey) {
2050
+ throw new Error("ctx.invokeChild requires AGENT_COMPOSE_URL and AGENT_COMPOSE_API_KEY");
2051
+ }
2052
+ childClient = new AgentComposeClient(baseUrl, apiKey);
2053
+ return childClient;
2054
+ };
2055
+ return (name, input, childOpts) => getChildClient().invokeAndWait(name, input, {
2056
+ ...childOpts,
2057
+ parentRunId: runId,
2058
+ factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default"
2059
+ });
2060
+ }
2061
+ // src/workflow-steps/step.ts
2062
+ function defineStep(opts) {
2063
+ if (!opts.name)
2064
+ throw new Error("defineStep: 'name' is required");
2065
+ return {
2066
+ name: opts.name,
2067
+ input: opts.input,
2068
+ output: opts.output,
2069
+ run: opts.run
2070
+ };
2071
+ }
2072
+ // src/step-invocation/protocol.ts
2073
+ var STEP_RESULT_PREFIX = "__AC_STEP_RESULT__";
2074
+ function stepResultLinePrefix(token) {
2075
+ return `${STEP_RESULT_PREFIX}${token}:`;
2076
+ }
2077
+ var RUNNER_BUNDLE_PATH = "/tmp/runner.bundle.js";
2078
+ var RUNNER_COMMAND = `node ${RUNNER_BUNDLE_PATH}`;
2079
+ var STEP_ENV = {
2080
+ RUN_ID: "RUN_ID",
2081
+ STEP_MODE: "AC_STEP_MODE",
2082
+ STEP_INDEX: "AC_STEP_INDEX",
2083
+ STEP_INPUT_PATH: "AC_STEP_INPUT_PATH",
2084
+ REQUEST_CONTEXT_PATH: "AC_REQUEST_CONTEXT_PATH",
2085
+ STEP_RESULT_TOKEN: "AC_STEP_RESULT_TOKEN"
2086
+ };
2087
+ function stepInputPath(stepIndex) {
2088
+ return `/tmp/wf/step-input-${stepIndex}.json`;
2089
+ }
2090
+ function requestContextPath(stepIndex) {
2091
+ return `/tmp/wf/request-context-${stepIndex}.json`;
2092
+ }
2093
+ // src/step-invocation/invoker.ts
2094
+ import { randomBytes } from "node:crypto";
2095
+ function buildStepEnvs(args) {
2096
+ return {
2097
+ [STEP_ENV.RUN_ID]: args.runId,
2098
+ [STEP_ENV.STEP_MODE]: "1",
2099
+ [STEP_ENV.STEP_INDEX]: String(args.stepIndex),
2100
+ [STEP_ENV.STEP_INPUT_PATH]: stepInputPath(args.stepIndex),
2101
+ [STEP_ENV.REQUEST_CONTEXT_PATH]: requestContextPath(args.stepIndex),
2102
+ [STEP_ENV.STEP_RESULT_TOKEN]: args.resultToken
2103
+ };
2104
+ }
2105
+ function parseStepResult(stdout, resultToken) {
2106
+ const prefix = stepResultLinePrefix(resultToken);
2107
+ const line = stdout.split(/\r?\n/).find((l) => l.startsWith(prefix));
2108
+ if (!line)
2109
+ return null;
2110
+ let parsed;
2111
+ try {
2112
+ parsed = JSON.parse(line.slice(prefix.length));
2113
+ } catch {
2114
+ return { ok: false, error: { kind: "protocol", message: "step result JSON parse failed" } };
2115
+ }
2116
+ if (parsed.ok) {
2117
+ return parsed.observability ? { ok: true, output: parsed.output, observability: parsed.observability } : { ok: true, output: parsed.output };
2118
+ }
2119
+ const kind = parsed.kind === "user-step" ? "user-step" : "protocol";
2120
+ const baseMessage = parsed.error ?? "step body failed without a message";
2121
+ const message = parsed.kind && parsed.kind !== "user-step" && parsed.kind !== "protocol" ? `(unknown kind "${parsed.kind}") ${baseMessage}` : baseMessage;
2122
+ return { ok: false, error: { kind, message } };
2123
+ }
2124
+ async function invokeStep(sandbox, request, opts) {
2125
+ const resultToken = randomBytes(16).toString("hex");
2126
+ await Promise.all([
2127
+ sandbox.files.write(stepInputPath(request.stepIndex), JSON.stringify(request.input)),
2128
+ sandbox.files.write(requestContextPath(request.stepIndex), JSON.stringify(request.requestContext))
2129
+ ]);
2130
+ const envs = {
2131
+ ...opts?.envs ?? {},
2132
+ ...buildStepEnvs({
2133
+ runId: request.runId,
2134
+ stepIndex: request.stepIndex,
2135
+ resultToken
2136
+ })
2137
+ };
2138
+ const sentinelPrefix = stepResultLinePrefix(resultToken);
2139
+ const makeLineSplitter = (sink, filterSentinel) => {
2140
+ if (!sink)
2141
+ return { onChunk: undefined, flush: () => {} };
2142
+ let buf = "";
2143
+ return {
2144
+ onChunk: (chunk) => {
2145
+ buf += chunk;
2146
+ let nl;
2147
+ while ((nl = buf.indexOf(`
2148
+ `)) !== -1) {
2149
+ const line = buf.slice(0, nl);
2150
+ buf = buf.slice(nl + 1);
2151
+ if (filterSentinel && line.startsWith(sentinelPrefix))
2152
+ continue;
2153
+ sink(line);
1094
2154
  }
2155
+ },
2156
+ flush: () => {
2157
+ if (buf.length === 0)
2158
+ return;
2159
+ const line = buf;
2160
+ buf = "";
2161
+ if (filterSentinel && line.startsWith(sentinelPrefix))
2162
+ return;
2163
+ sink(line);
2164
+ }
2165
+ };
2166
+ };
2167
+ const stdoutSplitter = makeLineSplitter(opts?.onStdout, true);
2168
+ const stderrSplitter = makeLineSplitter(opts?.onStderr, false);
2169
+ const result = await sandbox.commands.run(RUNNER_COMMAND, {
2170
+ envs,
2171
+ timeoutMs: 0,
2172
+ ...stdoutSplitter.onChunk ? { onStdout: stdoutSplitter.onChunk } : {},
2173
+ ...stderrSplitter.onChunk ? { onStderr: stderrSplitter.onChunk } : {}
2174
+ });
2175
+ stdoutSplitter.flush();
2176
+ stderrSplitter.flush();
2177
+ const parsed = parseStepResult(result.stdout, resultToken);
2178
+ if (parsed)
2179
+ return parsed;
2180
+ if (result.exitCode !== 0) {
2181
+ return {
2182
+ ok: false,
2183
+ error: {
2184
+ kind: "runner-exit",
2185
+ message: `runner subprocess exited ${result.exitCode} before emitting a step result`,
2186
+ exitCode: result.exitCode
1095
2187
  }
1096
- }, sandbox);
2188
+ };
2189
+ }
2190
+ return {
2191
+ ok: false,
2192
+ error: { kind: "protocol", message: "no tokenised step result found on stdout" }
2193
+ };
2194
+ }
2195
+ // src/step-invocation/server.ts
2196
+ import { readFileSync } from "node:fs";
2197
+ function emitResult(token, payload) {
2198
+ return new Promise((resolve, reject) => {
2199
+ const line = `${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}
2200
+ `;
2201
+ process.stdout.write(line, (err) => {
2202
+ if (err)
2203
+ reject(err);
2204
+ else
2205
+ resolve();
2206
+ });
2207
+ });
2208
+ }
2209
+ function scrubProtocolEnvs() {
2210
+ for (const key of [
2211
+ STEP_ENV.STEP_MODE,
2212
+ STEP_ENV.STEP_INDEX,
2213
+ STEP_ENV.STEP_INPUT_PATH,
2214
+ STEP_ENV.REQUEST_CONTEXT_PATH,
2215
+ STEP_ENV.STEP_RESULT_TOKEN
2216
+ ]) {
2217
+ delete process.env[key];
2218
+ }
2219
+ }
2220
+ async function serveStep(handler) {
2221
+ const token = process.env[STEP_ENV.STEP_RESULT_TOKEN];
2222
+ if (!token) {
2223
+ process.stderr.write(`[step-server] missing ${STEP_ENV.STEP_RESULT_TOKEN}
2224
+ `);
2225
+ process.exit(1);
2226
+ }
2227
+ let exitCode = 0;
2228
+ let payload;
2229
+ let setupResult;
2230
+ try {
2231
+ const runId = process.env[STEP_ENV.RUN_ID];
2232
+ const stepIndexEnv = process.env[STEP_ENV.STEP_INDEX];
2233
+ const inputPath = process.env[STEP_ENV.STEP_INPUT_PATH];
2234
+ const ctxPath = process.env[STEP_ENV.REQUEST_CONTEXT_PATH];
2235
+ const stepIndex = stepIndexEnv === undefined ? Number.NaN : Number(stepIndexEnv);
2236
+ if (!runId || !inputPath || !ctxPath || !Number.isFinite(stepIndex)) {
2237
+ throw new Error(`step-server missing required env: ${[
2238
+ !runId && STEP_ENV.RUN_ID,
2239
+ !Number.isFinite(stepIndex) && STEP_ENV.STEP_INDEX,
2240
+ !inputPath && STEP_ENV.STEP_INPUT_PATH,
2241
+ !ctxPath && STEP_ENV.REQUEST_CONTEXT_PATH
2242
+ ].filter(Boolean).join(", ")}`);
2243
+ }
2244
+ const input = JSON.parse(readFileSync(inputPath, "utf8"));
2245
+ const requestContext = JSON.parse(readFileSync(ctxPath, "utf8"));
2246
+ setupResult = { runId, stepIndex, input, requestContext };
1097
2247
  } catch (err) {
1098
- workflowErr = err;
2248
+ exitCode = 1;
2249
+ payload = { ok: false, kind: "protocol", error: err instanceof Error ? err.message : String(err) };
2250
+ }
2251
+ if (setupResult) {
2252
+ scrubProtocolEnvs();
2253
+ try {
2254
+ const { output, observability } = await handler(setupResult);
2255
+ payload = observability ? { ok: true, output, observability } : { ok: true, output };
2256
+ } catch (err) {
2257
+ exitCode = 1;
2258
+ payload = { ok: false, kind: "user-step", error: err instanceof Error ? err.message : String(err) };
2259
+ }
1099
2260
  }
1100
- if (workflowErr !== undefined) {
1101
- if (workflowErr instanceof WorkflowError)
1102
- throw workflowErr;
1103
- throw new WorkflowError(formatError(workflowErr), { cause: workflowErr });
2261
+ await emitResult(token, payload);
2262
+ process.exit(exitCode);
2263
+ }
2264
+ // src/step-invocation/types.ts
2265
+ class StepExecutionError extends Error {
2266
+ kind;
2267
+ exitCode;
2268
+ constructor(error, stepLabel) {
2269
+ super(`[${error.kind}] ${stepLabel}: ${error.message}`);
2270
+ this.name = "StepExecutionError";
2271
+ this.kind = error.kind;
2272
+ this.exitCode = error.kind === "runner-exit" ? error.exitCode : undefined;
1104
2273
  }
1105
- return { response: wfOutput ?? null };
1106
2274
  }
1107
2275
  // src/agent/run-agent.ts
1108
- import { z as z3 } from "zod";
2276
+ import { z as z6 } from "zod";
1109
2277
 
1110
2278
  // src/agent/protocol-suffix.md
1111
2279
  var protocol_suffix_default = '## Status Signal\n\nWhen you have finished your work or are blocked, emit a `<status>` block at the end of your response:\n\n```json\n<status>\n{\n "summary": "one sentence describing what was done or what is blocking",\n "completed": ["each acceptance criterion that is now fully met"],\n "blockers": [],\n "changed_files": ["relative/path/to/file"],\n "tests_run": true,\n "exit_signal": true\n}\n</status>\n```\n\n**Field semantics:**\n- `summary`: one sentence — what was accomplished or what is blocking\n- `completed`: acceptance criteria items that are fully done — be specific\n- `blockers`: non-empty when `exit_signal: false` — describe the exact obstacle\n- `changed_files`: relative paths of files you created or modified\n- `tests_run`: `true` if you ran any test suite (pass or fail); `false` if no tests exist or you skipped them\n- `exit_signal: true` — set when ALL acceptance criteria are met and no blockers remain\n- `exit_signal: false` — set when blocked or unfinished; `blockers` must be non-empty\n\n**If you are still actively working** and have not reached a natural stopping point, do NOT emit a `<status>` block — just keep working.\n\n**Example (done):**\n\n```json\n<status>\n{\n "summary": "Added input validation middleware to /api/tasks with tests",\n "completed": ["POST /api/tasks validates required fields", "Returns 400 with details on invalid input", "Unit tests passing"],\n "blockers": [],\n "changed_files": ["src/middleware/validate.ts", "src/routes/tasks.ts", "tests/validate.test.ts"],\n "tests_run": true,\n "exit_signal": true\n}\n</status>\n```\n\n**Example (blocked):**\n\n```json\n<status>\n{\n "summary": "Implemented middleware but tests are failing due to module resolution",\n "completed": ["Middleware created and wired into route"],\n "blockers": ["Tests fail: cannot resolve import \'./validate\' — module resolution config unclear"],\n "changed_files": ["src/middleware/validate.ts"],\n "tests_run": true,\n "exit_signal": false\n}\n</status>\n```\n';
@@ -1119,33 +2287,30 @@ function stripFrontmatter(content) {
1119
2287
  ---`, 3);
1120
2288
  return end === -1 ? content : content.slice(end + 4).trimStart();
1121
2289
  }
1122
- function inject(template, vars) {
1123
- return Object.entries(vars).reduce((t, [k, v]) => t.replaceAll(`{{${k}}}`, v), template);
1124
- }
1125
2290
  function zodTypeName(schema) {
1126
- if (schema instanceof z3.ZodString)
2291
+ if (schema instanceof z6.ZodString)
1127
2292
  return "string";
1128
- if (schema instanceof z3.ZodNumber)
2293
+ if (schema instanceof z6.ZodNumber)
1129
2294
  return "number";
1130
- if (schema instanceof z3.ZodBoolean)
2295
+ if (schema instanceof z6.ZodBoolean)
1131
2296
  return "boolean";
1132
- if (schema instanceof z3.ZodArray)
2297
+ if (schema instanceof z6.ZodArray)
1133
2298
  return `${zodTypeName(schema.element)}[]`;
1134
- if (schema instanceof z3.ZodEnum)
2299
+ if (schema instanceof z6.ZodEnum)
1135
2300
  return schema.options.map((o) => `"${o}"`).join(" | ");
1136
- if (schema instanceof z3.ZodOptional)
2301
+ if (schema instanceof z6.ZodOptional)
1137
2302
  return `${zodTypeName(schema.unwrap())} (optional)`;
1138
- if (schema instanceof z3.ZodNullable)
2303
+ if (schema instanceof z6.ZodNullable)
1139
2304
  return `${zodTypeName(schema.unwrap())} | null`;
1140
- if (schema instanceof z3.ZodLiteral)
2305
+ if (schema instanceof z6.ZodLiteral)
1141
2306
  return `"${String(schema.value)}"`;
1142
- if (schema instanceof z3.ZodObject)
2307
+ if (schema instanceof z6.ZodObject)
1143
2308
  return "object";
1144
2309
  return "any";
1145
2310
  }
1146
2311
  function buildResponseFormatAppendix(schema) {
1147
2312
  const header = ["## Response Format", "", "When you emit `exit_signal: true` in your `<status>` block, also include a `<response>` block containing JSON immediately after `<status>`:", ""];
1148
- const shape = schema instanceof z3.ZodObject ? schema.shape : null;
2313
+ const shape = schema instanceof z6.ZodObject ? schema.shape : null;
1149
2314
  const body = shape ? (() => {
1150
2315
  const entries = Object.entries(shape);
1151
2316
  const example = Object.fromEntries(entries.map(([k, v]) => [k, `<${zodTypeName(v)}>`]));
@@ -1170,11 +2335,11 @@ function buildResponseFormatAppendix(schema) {
1170
2335
  return [...header, ...body].join(`
1171
2336
  `);
1172
2337
  }
1173
- async function runAgent(opts) {
2338
+ async function agent(opts) {
1174
2339
  const workingDir = opts.workingDir ?? "";
1175
- const promptVars = { WORKING_DIR: workingDir, DIFF_BASE: "HEAD~1", ...opts.promptVars };
1176
2340
  return agentLoop({
1177
2341
  runtime: (runtimeOpts) => opts.runtime.create(opts.sandbox, runtimeOpts),
2342
+ ...opts.agentId !== undefined ? { agentId: opts.agentId } : {},
1178
2343
  ...opts.label !== undefined ? { label: opts.label } : {},
1179
2344
  ...opts.budget?.turnsPerIteration !== undefined ? { turnsPerIteration: opts.budget.turnsPerIteration } : {},
1180
2345
  ...opts.budget?.maxIterations !== undefined ? { maxIterations: opts.budget.maxIterations } : {},
@@ -1184,7 +2349,7 @@ async function runAgent(opts) {
1184
2349
  buildPrompt: (_lastStatus, iteration) => {
1185
2350
  if (iteration > 0)
1186
2351
  return PROTOCOL_SUFFIX;
1187
- const base = inject(stripFrontmatter(opts.prompt), promptVars);
2352
+ const base = stripFrontmatter(opts.prompt);
1188
2353
  return opts.responseSchema ? `${base}
1189
2354
 
1190
2355
  ${PROTOCOL_SUFFIX}
@@ -1193,8 +2358,20 @@ ${buildResponseFormatAppendix(opts.responseSchema)}` : `${base}
1193
2358
 
1194
2359
  ${PROTOCOL_SUFFIX}`;
1195
2360
  },
2361
+ ...opts.events ? { onAgentLifecycleEvent: (event) => {
2362
+ opts.events?.emit(event);
2363
+ } } : {},
1196
2364
  ...opts.onAgentEvent ? { onAgentEvent: opts.onAgentEvent } : {},
1197
- ...opts.onIteration ? { onIteration: opts.onIteration } : {}
2365
+ ...opts.onIteration ? { onIteration: opts.onIteration } : {},
2366
+ ...opts.processors?.length ? { processors: opts.processors } : {},
2367
+ requestContext: opts.requestContext ?? RequestContext.fromReserved({
2368
+ teamId: "",
2369
+ runId: "",
2370
+ workflowId: "",
2371
+ factoryId: null,
2372
+ apiKeyScopes: [],
2373
+ parentRunId: null
2374
+ })
1198
2375
  });
1199
2376
  }
1200
2377
  // src/runtimes/openai-desktop.ts