@namzu/sdk 50.0.0 → 51.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/execution/code-runtime/types.d.ts +35 -0
  3. package/dist/execution/code-runtime/types.d.ts.map +1 -1
  4. package/dist/execution/code-runtime/types.js.map +1 -1
  5. package/dist/execution/code-runtime/worker-program.d.ts +1 -1
  6. package/dist/execution/code-runtime/worker-program.d.ts.map +1 -1
  7. package/dist/execution/code-runtime/worker-program.js +72 -13
  8. package/dist/execution/code-runtime/worker-program.js.map +1 -1
  9. package/dist/execution/code-runtime/worker.d.ts.map +1 -1
  10. package/dist/execution/code-runtime/worker.js +35 -1
  11. package/dist/execution/code-runtime/worker.js.map +1 -1
  12. package/dist/execution/workflow/errors.d.ts +5 -0
  13. package/dist/execution/workflow/errors.d.ts.map +1 -0
  14. package/dist/execution/workflow/errors.js +8 -0
  15. package/dist/execution/workflow/errors.js.map +1 -0
  16. package/dist/execution/workflow/index.d.ts +9 -0
  17. package/dist/execution/workflow/index.d.ts.map +1 -0
  18. package/dist/execution/workflow/index.js +6 -0
  19. package/dist/execution/workflow/index.js.map +1 -0
  20. package/dist/execution/workflow/journal.d.ts +18 -0
  21. package/dist/execution/workflow/journal.d.ts.map +1 -0
  22. package/dist/execution/workflow/journal.js +43 -0
  23. package/dist/execution/workflow/journal.js.map +1 -0
  24. package/dist/execution/workflow/meta.d.ts +14 -0
  25. package/dist/execution/workflow/meta.d.ts.map +1 -0
  26. package/dist/execution/workflow/meta.js +314 -0
  27. package/dist/execution/workflow/meta.js.map +1 -0
  28. package/dist/execution/workflow/phases.d.ts +32 -0
  29. package/dist/execution/workflow/phases.d.ts.map +1 -0
  30. package/dist/execution/workflow/phases.js +57 -0
  31. package/dist/execution/workflow/phases.js.map +1 -0
  32. package/dist/execution/workflow/prelude.d.ts +15 -0
  33. package/dist/execution/workflow/prelude.d.ts.map +1 -0
  34. package/dist/execution/workflow/prelude.js +128 -0
  35. package/dist/execution/workflow/prelude.js.map +1 -0
  36. package/dist/execution/workflow/runtime.d.ts +31 -0
  37. package/dist/execution/workflow/runtime.d.ts.map +1 -0
  38. package/dist/execution/workflow/runtime.js +682 -0
  39. package/dist/execution/workflow/runtime.js.map +1 -0
  40. package/dist/execution/workflow/schema.d.ts +10 -0
  41. package/dist/execution/workflow/schema.d.ts.map +1 -0
  42. package/dist/execution/workflow/schema.js +145 -0
  43. package/dist/execution/workflow/schema.js.map +1 -0
  44. package/dist/execution/workflow/types.d.ts +267 -0
  45. package/dist/execution/workflow/types.d.ts.map +1 -0
  46. package/dist/execution/workflow/types.js +7 -0
  47. package/dist/execution/workflow/types.js.map +1 -0
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/plugin/shell-hook.d.ts.map +1 -1
  53. package/dist/plugin/shell-hook.js +2 -1
  54. package/dist/plugin/shell-hook.js.map +1 -1
  55. package/dist/process/kill-tree.d.ts +7 -0
  56. package/dist/process/kill-tree.d.ts.map +1 -1
  57. package/dist/process/kill-tree.js +41 -0
  58. package/dist/process/kill-tree.js.map +1 -1
  59. package/dist/provider/strict-schema.d.ts +28 -0
  60. package/dist/provider/strict-schema.d.ts.map +1 -1
  61. package/dist/provider/strict-schema.js +86 -0
  62. package/dist/provider/strict-schema.js.map +1 -1
  63. package/dist/provider/token-budget.d.ts.map +1 -1
  64. package/dist/provider/token-budget.js +31 -2
  65. package/dist/provider/token-budget.js.map +1 -1
  66. package/dist/public-runtime.d.ts +2 -1
  67. package/dist/public-runtime.d.ts.map +1 -1
  68. package/dist/public-runtime.js +4 -1
  69. package/dist/public-runtime.js.map +1 -1
  70. package/dist/public-tools.d.ts +1 -1
  71. package/dist/public-tools.d.ts.map +1 -1
  72. package/dist/public-tools.js +1 -1
  73. package/dist/public-tools.js.map +1 -1
  74. package/dist/public-types.d.ts +3 -0
  75. package/dist/public-types.d.ts.map +1 -1
  76. package/dist/runtime/jobs/job-id.d.ts +21 -0
  77. package/dist/runtime/jobs/job-id.d.ts.map +1 -0
  78. package/dist/runtime/jobs/job-id.js +66 -0
  79. package/dist/runtime/jobs/job-id.js.map +1 -0
  80. package/dist/runtime/jobs/registry.d.ts +0 -1
  81. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  82. package/dist/runtime/jobs/registry.js +2 -3
  83. package/dist/runtime/jobs/registry.js.map +1 -1
  84. package/dist/runtime/query/prompt.d.ts.map +1 -1
  85. package/dist/runtime/query/prompt.js +5 -1
  86. package/dist/runtime/query/prompt.js.map +1 -1
  87. package/dist/tools/builtins/bash.d.ts +8 -1
  88. package/dist/tools/builtins/bash.d.ts.map +1 -1
  89. package/dist/tools/builtins/bash.js +20 -3
  90. package/dist/tools/builtins/bash.js.map +1 -1
  91. package/dist/tools/builtins/edit.d.ts.map +1 -1
  92. package/dist/tools/builtins/edit.js +12 -1
  93. package/dist/tools/builtins/edit.js.map +1 -1
  94. package/dist/tools/builtins/job.d.ts.map +1 -1
  95. package/dist/tools/builtins/job.js +4 -0
  96. package/dist/tools/builtins/job.js.map +1 -1
  97. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  98. package/dist/tools/builtins/wait-for-job.js +4 -0
  99. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  100. package/dist/tools/command-shell.d.ts +55 -0
  101. package/dist/tools/command-shell.d.ts.map +1 -1
  102. package/dist/tools/command-shell.js +174 -13
  103. package/dist/tools/command-shell.js.map +1 -1
  104. package/package.json +1 -1
  105. package/src/agents/AGENTS.md +134 -46
  106. package/src/execution/code-runtime/types.ts +37 -1
  107. package/src/execution/code-runtime/worker-program.ts +72 -13
  108. package/src/execution/code-runtime/worker.ts +40 -1
  109. package/src/execution/workflow/errors.ts +7 -0
  110. package/src/execution/workflow/index.ts +8 -0
  111. package/src/execution/workflow/journal.ts +46 -0
  112. package/src/execution/workflow/meta.ts +316 -0
  113. package/src/execution/workflow/phases.ts +91 -0
  114. package/src/execution/workflow/prelude.ts +135 -0
  115. package/src/execution/workflow/runtime.ts +786 -0
  116. package/src/execution/workflow/schema.ts +137 -0
  117. package/src/execution/workflow/types.ts +300 -0
  118. package/src/index.ts +4 -0
  119. package/src/plugin/shell-hook.ts +2 -1
  120. package/src/process/kill-tree.ts +43 -0
  121. package/src/provider/strict-schema.ts +90 -0
  122. package/src/provider/token-budget.ts +31 -1
  123. package/src/public-runtime.ts +12 -0
  124. package/src/public-tools.ts +6 -0
  125. package/src/public-types.ts +3 -0
  126. package/src/runtime/jobs/job-id.ts +66 -0
  127. package/src/runtime/jobs/registry.ts +2 -3
  128. package/src/runtime/query/prompt.ts +5 -1
  129. package/src/tools/builtins/bash.ts +33 -3
  130. package/src/tools/builtins/edit.ts +12 -1
  131. package/src/tools/builtins/job.ts +4 -0
  132. package/src/tools/builtins/wait-for-job.ts +4 -0
  133. package/src/tools/command-shell.ts +194 -11
@@ -0,0 +1,786 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { cpus } from 'node:os'
3
+
4
+ import { encodeCodeValue } from '../code-runtime/json-value.js'
5
+ import type { CodeFrame, CodeRuntime, HostCallResult } from '../code-runtime/types.js'
6
+ import { WorkerCodeRuntime } from '../code-runtime/worker.js'
7
+ import { WorkflowScriptError } from './errors.js'
8
+ import { preview, sha256 } from './journal.js'
9
+ import { splitWorkflowScript } from './meta.js'
10
+ import { type PhaseAgentView, PhaseRegistry, summarizeWorkflowPhases } from './phases.js'
11
+ import { buildPrelude, preludeLineCount } from './prelude.js'
12
+ import { parseJsonText, validateAgainstSchema } from './schema.js'
13
+ import type {
14
+ WorkflowAgentEvent,
15
+ WorkflowAgentOutcome,
16
+ WorkflowAgentProgress,
17
+ WorkflowAgentState,
18
+ WorkflowEvent,
19
+ WorkflowFinishStatus,
20
+ WorkflowJournalRecord,
21
+ WorkflowLimits,
22
+ WorkflowResolvedLimits,
23
+ WorkflowRunOptions,
24
+ WorkflowRunResult,
25
+ WorkflowUsage,
26
+ } from './types.js'
27
+
28
+ /** The one capability a script has. */
29
+ export const WORKFLOW_HOST_CALL = 'agent'
30
+
31
+ const MAX_ARGS_BYTES = 64 * 1024
32
+ const MAX_PROMPT_BYTES = 200 * 1024
33
+ const MAX_SCHEMA_BYTES = 32 * 1024
34
+ /** Printed lines (`log()` and `phase()` records) the interpreter may emit. */
35
+ const MAX_LOG_OUTPUT_BYTES = 256 * 1024
36
+ /** Headroom for the prelude and the serialized `args` on top of the script. */
37
+ const SOURCE_OVERHEAD_BYTES = 16 * 1024 + MAX_ARGS_BYTES
38
+ const AGENT_CALL_KEYS = new Set([
39
+ 'prompt',
40
+ 'cursor',
41
+ 'label',
42
+ 'phase',
43
+ 'schema',
44
+ 'agentType',
45
+ 'model',
46
+ 'effort',
47
+ ])
48
+
49
+ function integer(name: string, value: number, min: number, max: number): number {
50
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
51
+ throw new RangeError(`${name} must be an integer between ${min} and ${max}.`)
52
+ }
53
+ return value
54
+ }
55
+
56
+ export function resolveWorkflowLimits(limits: WorkflowLimits = {}): WorkflowResolvedLimits {
57
+ const defaultConcurrency = Math.min(8, Math.max(1, cpus().length - 2))
58
+ return {
59
+ maxConcurrency: integer('maxConcurrency', limits.maxConcurrency ?? defaultConcurrency, 1, 16),
60
+ maxAgents: integer('maxAgents', limits.maxAgents ?? 200, 1, 1000),
61
+ maxSourceBytes: integer(
62
+ 'maxSourceBytes',
63
+ limits.maxSourceBytes ?? 256 * 1024,
64
+ 1,
65
+ 4 * 1024 * 1024,
66
+ ),
67
+ memoryLimitBytes: integer(
68
+ 'memoryLimitBytes',
69
+ limits.memoryLimitBytes ?? 64 * 1024 * 1024,
70
+ 16 * 1024 * 1024,
71
+ 256 * 1024 * 1024,
72
+ ),
73
+ maxValueBytes: integer(
74
+ 'maxValueBytes',
75
+ limits.maxValueBytes ?? 1024 * 1024,
76
+ 1,
77
+ 16 * 1024 * 1024,
78
+ ),
79
+ ...(limits.tokenBudget !== undefined
80
+ ? { tokenBudget: integer('tokenBudget', limits.tokenBudget, 1, Number.MAX_SAFE_INTEGER) }
81
+ : {}),
82
+ maxDurationMs: integer(
83
+ 'maxDurationMs',
84
+ limits.maxDurationMs ?? 6 * 3_600_000,
85
+ 1,
86
+ 2_147_483_647,
87
+ ),
88
+ cpuSliceMs: integer('cpuSliceMs', limits.cpuSliceMs ?? 2_000, 1, 2_147_483_647),
89
+ cpuTotalMs: integer('cpuTotalMs', limits.cpuTotalMs ?? 60_000, 1, 2_147_483_647),
90
+ }
91
+ }
92
+
93
+ interface AgentCall {
94
+ readonly prompt: string
95
+ readonly cursor: string | null
96
+ readonly label?: string
97
+ readonly phase?: string
98
+ readonly schema?: Record<string, unknown>
99
+ readonly agentType?: string
100
+ readonly model?: string
101
+ readonly effort?: string
102
+ }
103
+
104
+ function boundedText(value: unknown, field: string, max: number): string | undefined {
105
+ if (value === undefined) return undefined
106
+ if (typeof value !== 'string' || value.length > max) {
107
+ throw new Error(`agent(): ${field} must be a string of at most ${max} characters.`)
108
+ }
109
+ return value
110
+ }
111
+
112
+ /** The host owns this check: the prelude is convenience, not enforcement. */
113
+ function parseAgentCall(input: unknown): AgentCall {
114
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
115
+ throw new Error('agent(): the request must be an object.')
116
+ }
117
+ const record = input as Record<string, unknown>
118
+ for (const key of Object.keys(record)) {
119
+ if (!AGENT_CALL_KEYS.has(key)) throw new Error(`agent(): unknown option "${key}".`)
120
+ }
121
+ const prompt = record.prompt
122
+ if (typeof prompt !== 'string' || prompt.length === 0) {
123
+ throw new Error('agent(): a prompt string is required.')
124
+ }
125
+ if (Buffer.byteLength(prompt) > MAX_PROMPT_BYTES) {
126
+ throw new Error(`agent(): the prompt is larger than ${MAX_PROMPT_BYTES} bytes.`)
127
+ }
128
+ const cursor = record.cursor === null || record.cursor === undefined ? null : record.cursor
129
+ if (cursor !== null && (typeof cursor !== 'string' || cursor.length > 120)) {
130
+ throw new Error('agent(): cursor must be a short string.')
131
+ }
132
+ let schema: Record<string, unknown> | undefined
133
+ if (record.schema !== undefined) {
134
+ if (
135
+ typeof record.schema !== 'object' ||
136
+ record.schema === null ||
137
+ Array.isArray(record.schema)
138
+ ) {
139
+ throw new Error('agent(): schema must be a JSON Schema object.')
140
+ }
141
+ let size: number
142
+ try {
143
+ size = JSON.stringify(record.schema).length
144
+ } catch {
145
+ throw new Error('agent(): schema must be plain JSON.')
146
+ }
147
+ if (size > MAX_SCHEMA_BYTES) {
148
+ throw new Error(`agent(): schema is larger than ${MAX_SCHEMA_BYTES} bytes.`)
149
+ }
150
+ schema = record.schema as Record<string, unknown>
151
+ }
152
+ const label = boundedText(record.label, 'label', 120)
153
+ const phase = boundedText(record.phase, 'phase', 120)
154
+ const agentType = boundedText(record.agentType, 'agentType', 120)
155
+ const model = boundedText(record.model, 'model', 120)
156
+ const effort = boundedText(record.effort, 'effort', 32)
157
+ return {
158
+ prompt,
159
+ cursor,
160
+ ...(label !== undefined ? { label } : {}),
161
+ ...(phase !== undefined ? { phase } : {}),
162
+ ...(schema !== undefined ? { schema } : {}),
163
+ ...(agentType !== undefined ? { agentType } : {}),
164
+ ...(model !== undefined ? { model } : {}),
165
+ ...(effort !== undefined ? { effort } : {}),
166
+ }
167
+ }
168
+
169
+ function count(value: unknown): number {
170
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0
171
+ }
172
+
173
+ interface AgentRecord {
174
+ readonly index: number
175
+ readonly label: string
176
+ readonly prompt: string
177
+ readonly phaseIndex?: number
178
+ readonly phaseTitle?: string
179
+ readonly agentType?: string
180
+ readonly model?: string
181
+ readonly queuedAt: number
182
+ readonly controller: AbortController
183
+ state: WorkflowAgentState
184
+ startedAt?: number
185
+ finishedAt?: number
186
+ lastProgressAt: number
187
+ tokens: number
188
+ toolCalls: number
189
+ lastToolName?: string
190
+ lastToolSummary?: string
191
+ resultPreview?: string
192
+ error?: string
193
+ skipped: boolean
194
+ userSkipped: boolean
195
+ }
196
+
197
+ interface Waiter {
198
+ readonly agent: AgentRecord
199
+ readonly admit: (admitted: boolean) => void
200
+ }
201
+
202
+ export interface WorkflowRuntimeOptions {
203
+ /** Test seam. Default: a fresh {@link WorkerCodeRuntime} per run, sized from the limits. */
204
+ readonly codeRuntime?: CodeRuntime
205
+ /** Clock, in epoch milliseconds. */
206
+ readonly now?: () => number
207
+ /** How long to wait for aborted agents to settle before reporting the run finished. Default 5000. */
208
+ readonly settleGraceMs?: number
209
+ }
210
+
211
+ const MAX_SOURCE_LINE_CHARS = 200
212
+
213
+ /**
214
+ * The failure text a model reads: the error, then where in ITS script it
215
+ * happened and the line itself. Interpreter lines count the prelude and the
216
+ * worker's wrapper, so the first frame that falls in the author's text is the
217
+ * one reported (the innermost frame may be a prelude helper that rejected a
218
+ * bad argument; the call site is the next one out).
219
+ */
220
+ function describeScriptFailure(
221
+ outcome: {
222
+ readonly error: string
223
+ readonly errorName?: string
224
+ readonly frames?: readonly CodeFrame[]
225
+ },
226
+ body: string,
227
+ preludeLines: number,
228
+ ): string {
229
+ const what =
230
+ outcome.errorName !== undefined && outcome.errorName !== ''
231
+ ? `${outcome.errorName}: ${outcome.error}`
232
+ : outcome.error
233
+ const frame = outcome.frames
234
+ ?.map((f) => ({ line: f.line - preludeLines, column: f.column }))
235
+ .find((f) => f.line >= 1)
236
+ if (frame === undefined) {
237
+ return `${what} — the interpreter gave no line number for this error.`
238
+ }
239
+ const text = (body.split('\n')[frame.line - 1] ?? '').trim().slice(0, MAX_SOURCE_LINE_CHARS)
240
+ return `${what} — line ${frame.line}, column ${frame.column}: ${text}`
241
+ }
242
+
243
+ /**
244
+ * Runs a workflow script: a program the MODEL wrote once that decides how
245
+ * many subagents run and in which order.
246
+ *
247
+ * The script is untrusted input. It runs in the `run_code` interpreter (a
248
+ * fresh QuickJS WASM context in a worker thread: no filesystem, network,
249
+ * process, modules or timers, JSON-only crossings, memory and CPU limits) and
250
+ * its single capability is `agent()`, which asks the host's `spawnAgent` to
251
+ * start a subagent through the same delegation path as the `Agent` tool.
252
+ * Scheduling, caps, the token budget, events and the journal are host-side.
253
+ */
254
+ export class WorkflowRuntime {
255
+ private readonly now: () => number
256
+
257
+ constructor(private readonly options: WorkflowRuntimeOptions = {}) {
258
+ this.now = options.now ?? Date.now
259
+ }
260
+
261
+ async run(options: WorkflowRunOptions): Promise<WorkflowRunResult> {
262
+ const limits = resolveWorkflowLimits(options.limits)
263
+ if (Buffer.byteLength(options.script) > limits.maxSourceBytes) {
264
+ throw new WorkflowScriptError(`The script is larger than ${limits.maxSourceBytes} bytes.`)
265
+ }
266
+ const { meta, body } = splitWorkflowScript(options.script)
267
+ const args = options.args === undefined ? {} : options.args
268
+ try {
269
+ encodeCodeValue(args, MAX_ARGS_BYTES)
270
+ } catch (error) {
271
+ throw new WorkflowScriptError(
272
+ `The workflow arguments cannot be used: ${error instanceof Error ? error.message : String(error)}`,
273
+ )
274
+ }
275
+
276
+ const now = this.now
277
+ const runId =
278
+ options.runId ?? `w${[...randomBytes(8)].map((b) => (b % 36).toString(36)).join('')}`
279
+ const startedAt = now()
280
+ const phases = new PhaseRegistry()
281
+ const agents = new Map<number, AgentRecord>()
282
+ const inFlight = new Set<Promise<unknown>>()
283
+ const waiters: Waiter[] = []
284
+ const controller = new AbortController()
285
+ let running = 0
286
+ let requested = 0
287
+ let fatal: string | undefined
288
+ let closed = false
289
+ let journalError: string | undefined
290
+ let scriptPhase: string | undefined
291
+ let journalChain: Promise<void> = Promise.resolve()
292
+
293
+ const journal = (record: WorkflowJournalRecord): void => {
294
+ const sink = options.journal
295
+ if (!sink || closed) return
296
+ journalChain = journalChain
297
+ .then(() => sink.append(record))
298
+ .catch((error: unknown) => {
299
+ journalError ??= error instanceof Error ? error.message : String(error)
300
+ })
301
+ }
302
+ const emit = (event: WorkflowEvent): void => {
303
+ if (closed) return
304
+ try {
305
+ options.onEvent?.(event)
306
+ } catch {
307
+ // A listener bug must not decide the run's outcome.
308
+ }
309
+ }
310
+ const setFatal = (reason: string): void => {
311
+ if (fatal !== undefined) return
312
+ fatal = reason
313
+ controller.abort(new Error(reason))
314
+ }
315
+ const totalTokens = (): number => {
316
+ let sum = 0
317
+ for (const a of agents.values()) sum += a.tokens
318
+ return sum
319
+ }
320
+ const announce = (rawTitle: string, declared: boolean): { index: number; title: string } => {
321
+ const { phase, isNew } = phases.register(rawTitle, declared)
322
+ if (isNew) {
323
+ const at = now()
324
+ emit({
325
+ type: 'workflow_phase',
326
+ runId,
327
+ index: phase.index,
328
+ title: phase.title,
329
+ declared,
330
+ at,
331
+ })
332
+ journal({ type: 'phase', at, index: phase.index, title: phase.title, declared })
333
+ }
334
+ return phase
335
+ }
336
+ const agentEvent = (agent: AgentRecord): WorkflowAgentEvent => ({
337
+ type: 'workflow_agent',
338
+ runId,
339
+ state: agent.state,
340
+ index: agent.index,
341
+ label: agent.label,
342
+ ...(agent.phaseIndex !== undefined ? { phaseIndex: agent.phaseIndex } : {}),
343
+ ...(agent.phaseTitle !== undefined ? { phaseTitle: agent.phaseTitle } : {}),
344
+ ...(agent.agentType !== undefined ? { agentType: agent.agentType } : {}),
345
+ ...(agent.model !== undefined ? { model: agent.model } : {}),
346
+ queuedAt: agent.queuedAt,
347
+ ...(agent.startedAt !== undefined ? { startedAt: agent.startedAt } : {}),
348
+ ...(agent.finishedAt !== undefined ? { finishedAt: agent.finishedAt } : {}),
349
+ lastProgressAt: agent.lastProgressAt,
350
+ tokens: agent.tokens,
351
+ toolCalls: agent.toolCalls,
352
+ ...(agent.lastToolName !== undefined ? { lastToolName: agent.lastToolName } : {}),
353
+ ...(agent.lastToolSummary !== undefined ? { lastToolSummary: agent.lastToolSummary } : {}),
354
+ promptPreview: preview(agent.prompt, 200),
355
+ ...(agent.resultPreview !== undefined ? { resultPreview: agent.resultPreview } : {}),
356
+ ...(agent.error !== undefined ? { error: agent.error } : {}),
357
+ ...(agent.skipped ? { skipped: true } : {}),
358
+ })
359
+
360
+ // FIFO admission: an agent starts when fewer than maxConcurrency run.
361
+ const acquire = (agent: AgentRecord): Promise<boolean> => {
362
+ if (running < limits.maxConcurrency && waiters.length === 0) {
363
+ running++
364
+ return Promise.resolve(true)
365
+ }
366
+ return new Promise<boolean>((admit) => {
367
+ waiters.push({ agent, admit })
368
+ })
369
+ }
370
+ const release = (): void => {
371
+ running--
372
+ while (running < limits.maxConcurrency && waiters.length > 0) {
373
+ const next = waiters.shift() as Waiter
374
+ running++
375
+ next.admit(true)
376
+ }
377
+ }
378
+ const cancelQueued = (agent: AgentRecord): boolean => {
379
+ const at = waiters.findIndex((w) => w.agent === agent)
380
+ if (at === -1) return false
381
+ const [waiter] = waiters.splice(at, 1)
382
+ waiter?.admit(false)
383
+ return true
384
+ }
385
+ const abortAgents = (): void => {
386
+ for (const waiter of waiters.splice(0)) waiter.admit(false)
387
+ for (const agent of agents.values()) {
388
+ if (agent.finishedAt === undefined) agent.controller.abort(controller.signal.reason)
389
+ }
390
+ }
391
+ controller.signal.addEventListener('abort', abortAgents, { once: true })
392
+ const onUserAbort = (): void => controller.abort(options.signal?.reason)
393
+ if (options.signal?.aborted) onUserAbort()
394
+ else options.signal?.addEventListener('abort', onUserAbort, { once: true })
395
+
396
+ options.onControl?.({
397
+ skipAgent: (index) => {
398
+ const agent = agents.get(index)
399
+ if (!agent || agent.finishedAt !== undefined) return false
400
+ agent.userSkipped = true
401
+ if (!cancelQueued(agent)) agent.controller.abort(new Error('The agent was skipped.'))
402
+ return true
403
+ },
404
+ })
405
+
406
+ const settle = (
407
+ agent: AgentRecord,
408
+ outcome: { state: 'done' | 'error'; skipped?: boolean; value?: unknown; error?: string },
409
+ ): HostCallResult => {
410
+ const at = now()
411
+ agent.state = outcome.state
412
+ agent.finishedAt = at
413
+ agent.lastProgressAt = at
414
+ agent.skipped = outcome.skipped === true
415
+ if (outcome.error !== undefined) agent.error = outcome.error
416
+ let value: unknown =
417
+ outcome.state === 'done' && !outcome.skipped ? (outcome.value ?? null) : null
418
+ if (value !== null) {
419
+ agent.resultPreview = preview(typeof value === 'string' ? value : JSON.stringify(value))
420
+ }
421
+ try {
422
+ encodeCodeValue({ v: value, t: totalTokens() }, limits.maxValueBytes)
423
+ } catch {
424
+ agent.state = 'error'
425
+ agent.error = `Its result is larger than ${limits.maxValueBytes} bytes and could not be given to the script.`
426
+ agent.resultPreview = undefined
427
+ agent.skipped = false
428
+ value = null
429
+ }
430
+ emit(agentEvent(agent))
431
+ journal({
432
+ type: 'agent_outcome',
433
+ at,
434
+ index: agent.index,
435
+ state: agent.state === 'error' ? 'error' : agent.skipped ? 'skipped' : 'done',
436
+ tokens: agent.tokens,
437
+ toolCalls: agent.toolCalls,
438
+ queuedAt: agent.queuedAt,
439
+ ...(agent.startedAt !== undefined ? { startedAt: agent.startedAt } : {}),
440
+ finishedAt: at,
441
+ ...(agent.resultPreview !== undefined ? { resultPreview: agent.resultPreview } : {}),
442
+ ...(agent.error !== undefined ? { error: agent.error } : {}),
443
+ })
444
+ return { ok: true, value: { v: value, t: totalTokens() } }
445
+ }
446
+
447
+ const interpret = (
448
+ agent: AgentRecord,
449
+ call: AgentCall,
450
+ outcome: WorkflowAgentOutcome,
451
+ ): HostCallResult => {
452
+ agent.tokens = count(outcome.tokens)
453
+ agent.toolCalls = count(outcome.toolCalls)
454
+ if (agent.userSkipped || outcome.state === 'skipped') {
455
+ return settle(agent, { state: 'done', skipped: true })
456
+ }
457
+ if (controller.signal.aborted) {
458
+ return settle(agent, {
459
+ state: 'error',
460
+ error: 'The workflow was stopped before this agent finished.',
461
+ })
462
+ }
463
+ if (outcome.state === 'error') {
464
+ return settle(agent, { state: 'error', error: outcome.error ?? 'The agent failed.' })
465
+ }
466
+ if (call.schema === undefined) {
467
+ return settle(agent, { state: 'done', value: outcome.text ?? '' })
468
+ }
469
+ let candidate: unknown = outcome.data
470
+ if (candidate === undefined) {
471
+ const parsed = parseJsonText(outcome.text ?? '')
472
+ if (!parsed.ok) {
473
+ return settle(agent, {
474
+ state: 'error',
475
+ error: 'The agent was asked for structured output but its answer was not valid JSON.',
476
+ })
477
+ }
478
+ candidate = parsed.value
479
+ }
480
+ const problem = validateAgainstSchema(candidate, call.schema)
481
+ if (problem) {
482
+ return settle(agent, {
483
+ state: 'error',
484
+ error: `The agent's answer did not match the schema: ${problem}.`,
485
+ })
486
+ }
487
+ return settle(agent, { state: 'done', value: candidate })
488
+ }
489
+
490
+ const handleAgentCall = async (call: AgentCall): Promise<HostCallResult> => {
491
+ if (controller.signal.aborted) {
492
+ return { ok: false, error: fatal ?? 'The workflow was stopped.' }
493
+ }
494
+ if (requested >= limits.maxAgents) {
495
+ const reason = `This workflow tried to start more than ${limits.maxAgents} agents, which is the limit for one run. No agent past the limit was started.`
496
+ setFatal(reason)
497
+ return { ok: false, error: reason }
498
+ }
499
+ const index = ++requested
500
+ const phaseTitle = call.phase ?? call.cursor
501
+ const phase = phaseTitle?.trim() ? announce(phaseTitle, false) : undefined
502
+ const queuedAt = now()
503
+ const agent: AgentRecord = {
504
+ index,
505
+ label: call.label || `Agent ${index}`,
506
+ prompt: call.prompt,
507
+ ...(phase ? { phaseIndex: phase.index, phaseTitle: phase.title } : {}),
508
+ ...(call.agentType !== undefined ? { agentType: call.agentType } : {}),
509
+ ...(call.model !== undefined ? { model: call.model } : {}),
510
+ queuedAt,
511
+ controller: new AbortController(),
512
+ state: 'start',
513
+ lastProgressAt: queuedAt,
514
+ tokens: 0,
515
+ toolCalls: 0,
516
+ skipped: false,
517
+ userSkipped: false,
518
+ }
519
+ agents.set(index, agent)
520
+ emit(agentEvent(agent))
521
+ journal({
522
+ type: 'agent_request',
523
+ at: queuedAt,
524
+ index,
525
+ label: agent.label,
526
+ ...(phase ? { phaseIndex: phase.index, phaseTitle: phase.title } : {}),
527
+ promptPreview: preview(call.prompt),
528
+ promptSha256: sha256(call.prompt),
529
+ options: {
530
+ ...(call.agentType !== undefined ? { agentType: call.agentType } : {}),
531
+ ...(call.model !== undefined ? { model: call.model } : {}),
532
+ ...(call.effort !== undefined ? { effort: call.effort } : {}),
533
+ schema: call.schema !== undefined,
534
+ },
535
+ })
536
+
537
+ const admitted = await acquire(agent)
538
+ if (!admitted) {
539
+ return agent.userSkipped
540
+ ? settle(agent, { state: 'done', skipped: true })
541
+ : settle(agent, {
542
+ state: 'error',
543
+ error: 'The workflow was stopped before this agent started.',
544
+ })
545
+ }
546
+ try {
547
+ if (limits.tokenBudget !== undefined && totalTokens() >= limits.tokenBudget) {
548
+ return settle(agent, {
549
+ state: 'error',
550
+ error: `Not started: the token budget for this workflow (${limits.tokenBudget} tokens) is spent.`,
551
+ })
552
+ }
553
+ if (controller.signal.aborted || agent.userSkipped) {
554
+ return agent.userSkipped
555
+ ? settle(agent, { state: 'done', skipped: true })
556
+ : settle(agent, {
557
+ state: 'error',
558
+ error: 'The workflow was stopped before this agent started.',
559
+ })
560
+ }
561
+ agent.startedAt = now()
562
+ agent.lastProgressAt = agent.startedAt
563
+ emit(agentEvent(agent))
564
+ let outcome: WorkflowAgentOutcome
565
+ try {
566
+ outcome = await options.spawnAgent(
567
+ {
568
+ index,
569
+ label: agent.label,
570
+ prompt: call.prompt,
571
+ ...(phase ? { phaseIndex: phase.index, phaseTitle: phase.title } : {}),
572
+ ...(call.schema !== undefined ? { schema: call.schema } : {}),
573
+ ...(call.agentType !== undefined ? { agentType: call.agentType } : {}),
574
+ ...(call.model !== undefined ? { model: call.model } : {}),
575
+ ...(call.effort !== undefined ? { effort: call.effort } : {}),
576
+ onProgress: (progress: WorkflowAgentProgress) => {
577
+ if (agent.finishedAt !== undefined || closed) return
578
+ if (progress.tokens !== undefined) agent.tokens = count(progress.tokens)
579
+ if (progress.toolCalls !== undefined) agent.toolCalls = count(progress.toolCalls)
580
+ if (progress.lastToolName !== undefined) agent.lastToolName = progress.lastToolName
581
+ if (progress.lastToolSummary !== undefined) {
582
+ agent.lastToolSummary = preview(progress.lastToolSummary, 200)
583
+ }
584
+ agent.state = 'progress'
585
+ agent.lastProgressAt = now()
586
+ emit(agentEvent(agent))
587
+ },
588
+ },
589
+ agent.controller.signal,
590
+ )
591
+ } catch (error) {
592
+ outcome = {
593
+ state: 'error',
594
+ tokens: agent.tokens,
595
+ toolCalls: agent.toolCalls,
596
+ error: error instanceof Error ? error.message : String(error),
597
+ }
598
+ }
599
+ return interpret(agent, call, outcome)
600
+ } finally {
601
+ release()
602
+ }
603
+ }
604
+
605
+ const onOutput = (line: string): void => {
606
+ let record: unknown
607
+ try {
608
+ record = JSON.parse(line)
609
+ } catch {
610
+ return
611
+ }
612
+ if (typeof record !== 'object' || record === null) return
613
+ const { k, title, message } = record as Record<string, unknown>
614
+ if (k === 'phase' && typeof title === 'string' && title.trim()) {
615
+ announce(title, false)
616
+ scriptPhase = title.trim()
617
+ } else if (k === 'log' && typeof message === 'string') {
618
+ const at = now()
619
+ emit({ type: 'workflow_log', runId, message, at })
620
+ journal({ type: 'log', at, message })
621
+ }
622
+ }
623
+
624
+ // --- start ---
625
+ if (options.journal?.writeScript) {
626
+ try {
627
+ await options.journal.writeScript(options.script, sha256(options.script))
628
+ } catch (error) {
629
+ journalError = error instanceof Error ? error.message : String(error)
630
+ }
631
+ }
632
+ journal({
633
+ type: 'run_started',
634
+ at: startedAt,
635
+ runId,
636
+ scriptSha256: sha256(options.script),
637
+ scriptBytes: Buffer.byteLength(options.script),
638
+ meta,
639
+ args,
640
+ limits,
641
+ })
642
+ emit({
643
+ type: 'workflow_started',
644
+ runId,
645
+ name: meta.name,
646
+ description: meta.description,
647
+ phases: meta.phases,
648
+ at: startedAt,
649
+ })
650
+ for (const declared of meta.phases) announce(declared.title, true)
651
+
652
+ const codeRuntime =
653
+ this.options.codeRuntime ??
654
+ new WorkerCodeRuntime({
655
+ memoryLimitBytes: limits.memoryLimitBytes,
656
+ maxSourceBytes: limits.maxSourceBytes + SOURCE_OVERHEAD_BYTES,
657
+ maxValueBytes: limits.maxValueBytes,
658
+ maxHostCalls: limits.maxAgents + 100,
659
+ maxPendingHostCalls: limits.maxAgents + 100,
660
+ })
661
+
662
+ const preludeConfig = { args, tokenLimit: limits.tokenBudget ?? null }
663
+ const runResult = await codeRuntime.run({
664
+ source: `${buildPrelude(preludeConfig)}\n${body}`,
665
+ allowedCalls: [WORKFLOW_HOST_CALL],
666
+ timeoutMs: limits.maxDurationMs,
667
+ maxOutputBytes: MAX_LOG_OUTPUT_BYTES,
668
+ cpuLimits: { sliceMs: limits.cpuSliceMs, totalMs: limits.cpuTotalMs },
669
+ signal: controller.signal,
670
+ onOutput,
671
+ onHostCall: (request) => {
672
+ if (request.name !== WORKFLOW_HOST_CALL) {
673
+ return Promise.resolve({ ok: false, error: `Unknown host call "${request.name}".` })
674
+ }
675
+ let call: AgentCall
676
+ try {
677
+ call = parseAgentCall(request.input)
678
+ } catch (error) {
679
+ return Promise.resolve({
680
+ ok: false,
681
+ error: error instanceof Error ? error.message : String(error),
682
+ })
683
+ }
684
+ const pending = handleAgentCall(call).catch(
685
+ (error: unknown): HostCallResult => ({
686
+ ok: false,
687
+ error: error instanceof Error ? error.message : String(error),
688
+ }),
689
+ )
690
+ inFlight.add(pending)
691
+ void pending.finally(() => inFlight.delete(pending))
692
+ return pending
693
+ },
694
+ })
695
+
696
+ // --- finish ---
697
+ const userStopped = options.signal?.aborted === true && fatal === undefined
698
+ if (!controller.signal.aborted) controller.abort(new Error('The workflow finished.'))
699
+ options.signal?.removeEventListener('abort', onUserAbort)
700
+ if (inFlight.size > 0) {
701
+ const grace = this.options.settleGraceMs ?? 5_000
702
+ let timer: ReturnType<typeof setTimeout> | undefined
703
+ await Promise.race([
704
+ Promise.allSettled([...inFlight]),
705
+ new Promise<void>((resolve) => {
706
+ timer = setTimeout(resolve, grace)
707
+ }),
708
+ ])
709
+ clearTimeout(timer)
710
+ }
711
+
712
+ let status: WorkflowFinishStatus
713
+ let result: unknown
714
+ let error: string | undefined
715
+ const outcome = runResult.outcome
716
+ if (fatal !== undefined) {
717
+ status = 'failed'
718
+ error = fatal
719
+ } else if (outcome.status === 'completed') {
720
+ status = 'completed'
721
+ result = outcome.result
722
+ } else if (outcome.status === 'cancelled' || userStopped) {
723
+ status = 'stopped'
724
+ } else if (outcome.status === 'timed-out') {
725
+ status = 'failed'
726
+ error = `The script was stopped. It spent more than ${limits.cpuSliceMs} ms in one synchronous stretch, more than ${limits.cpuTotalMs} ms in all, or ran past the ${limits.maxDurationMs} ms limit for one run.`
727
+ } else {
728
+ status = 'failed'
729
+ const begun = [...agents.values()].filter((a) => a.startedAt !== undefined)
730
+ const finished = begun.filter((a) => a.state === 'done' && !a.skipped).length
731
+ const failedAgents = begun.filter((a) => a.state === 'error').length
732
+ const progress =
733
+ begun.length === 0
734
+ ? 'No agent had started.'
735
+ : `Before it failed: ${begun.length} agent${begun.length === 1 ? '' : 's'} started, ${finished} finished, ${failedAgents} failed. A rerun starts the whole script again, so finished agents run again.`
736
+ error = `${describeScriptFailure(outcome, body, preludeLineCount(preludeConfig))}\n${progress}${scriptPhase !== undefined ? ` Last phase: "${scriptPhase}".` : ''}`
737
+ }
738
+
739
+ const all = [...agents.values()]
740
+ const usage: WorkflowUsage = {
741
+ agents: all.length,
742
+ agentsDone: all.filter((a) => a.state === 'done' && !a.skipped).length,
743
+ agentsFailed: all.filter((a) => a.state === 'error').length,
744
+ agentsSkipped: all.filter((a) => a.skipped).length,
745
+ tokens: all.reduce((sum, a) => sum + a.tokens, 0),
746
+ toolCalls: all.reduce((sum, a) => sum + a.toolCalls, 0),
747
+ elapsedMs: Math.max(0, now() - startedAt),
748
+ }
749
+ const at = now()
750
+ emit({
751
+ type: 'workflow_finished',
752
+ runId,
753
+ status,
754
+ ...(status === 'completed' && result !== undefined ? { result } : {}),
755
+ ...(error !== undefined ? { error } : {}),
756
+ usage,
757
+ at,
758
+ })
759
+ journal({
760
+ type: 'run_finished',
761
+ at,
762
+ status,
763
+ ...(status === 'completed' && result !== undefined ? { result } : {}),
764
+ ...(error !== undefined ? { error } : {}),
765
+ usage,
766
+ })
767
+ await journalChain
768
+ closed = true
769
+
770
+ const views: PhaseAgentView[] = all.map((a) => ({
771
+ ...(a.phaseIndex !== undefined ? { phaseIndex: a.phaseIndex } : {}),
772
+ state: a.state,
773
+ tokens: a.tokens,
774
+ }))
775
+ return {
776
+ runId,
777
+ status,
778
+ ...(status === 'completed' && result !== undefined ? { result } : {}),
779
+ ...(error !== undefined ? { error } : {}),
780
+ usage,
781
+ phases: summarizeWorkflowPhases(meta.phases, phases.all(), views),
782
+ logsTruncated: runResult.outputTruncated,
783
+ ...(journalError !== undefined ? { journalError } : {}),
784
+ }
785
+ }
786
+ }