@namzu/sdk 50.1.0 → 51.1.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 (161) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  3. package/dist/bridge/sse/mapper.js +1 -0
  4. package/dist/bridge/sse/mapper.js.map +1 -1
  5. package/dist/execution/code-runtime/types.d.ts +35 -0
  6. package/dist/execution/code-runtime/types.d.ts.map +1 -1
  7. package/dist/execution/code-runtime/types.js.map +1 -1
  8. package/dist/execution/code-runtime/worker-program.d.ts +1 -1
  9. package/dist/execution/code-runtime/worker-program.d.ts.map +1 -1
  10. package/dist/execution/code-runtime/worker-program.js +72 -13
  11. package/dist/execution/code-runtime/worker-program.js.map +1 -1
  12. package/dist/execution/code-runtime/worker.d.ts.map +1 -1
  13. package/dist/execution/code-runtime/worker.js +35 -1
  14. package/dist/execution/code-runtime/worker.js.map +1 -1
  15. package/dist/execution/workflow/errors.d.ts +5 -0
  16. package/dist/execution/workflow/errors.d.ts.map +1 -0
  17. package/dist/execution/workflow/errors.js +8 -0
  18. package/dist/execution/workflow/errors.js.map +1 -0
  19. package/dist/execution/workflow/index.d.ts +9 -0
  20. package/dist/execution/workflow/index.d.ts.map +1 -0
  21. package/dist/execution/workflow/index.js +6 -0
  22. package/dist/execution/workflow/index.js.map +1 -0
  23. package/dist/execution/workflow/journal.d.ts +18 -0
  24. package/dist/execution/workflow/journal.d.ts.map +1 -0
  25. package/dist/execution/workflow/journal.js +43 -0
  26. package/dist/execution/workflow/journal.js.map +1 -0
  27. package/dist/execution/workflow/meta.d.ts +14 -0
  28. package/dist/execution/workflow/meta.d.ts.map +1 -0
  29. package/dist/execution/workflow/meta.js +314 -0
  30. package/dist/execution/workflow/meta.js.map +1 -0
  31. package/dist/execution/workflow/phases.d.ts +32 -0
  32. package/dist/execution/workflow/phases.d.ts.map +1 -0
  33. package/dist/execution/workflow/phases.js +57 -0
  34. package/dist/execution/workflow/phases.js.map +1 -0
  35. package/dist/execution/workflow/prelude.d.ts +15 -0
  36. package/dist/execution/workflow/prelude.d.ts.map +1 -0
  37. package/dist/execution/workflow/prelude.js +128 -0
  38. package/dist/execution/workflow/prelude.js.map +1 -0
  39. package/dist/execution/workflow/runtime.d.ts +31 -0
  40. package/dist/execution/workflow/runtime.d.ts.map +1 -0
  41. package/dist/execution/workflow/runtime.js +682 -0
  42. package/dist/execution/workflow/runtime.js.map +1 -0
  43. package/dist/execution/workflow/schema.d.ts +10 -0
  44. package/dist/execution/workflow/schema.d.ts.map +1 -0
  45. package/dist/execution/workflow/schema.js +145 -0
  46. package/dist/execution/workflow/schema.js.map +1 -0
  47. package/dist/execution/workflow/types.d.ts +267 -0
  48. package/dist/execution/workflow/types.d.ts.map +1 -0
  49. package/dist/execution/workflow/types.js +7 -0
  50. package/dist/execution/workflow/types.js.map +1 -0
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/plugin/shell-hook.d.ts.map +1 -1
  56. package/dist/plugin/shell-hook.js +2 -1
  57. package/dist/plugin/shell-hook.js.map +1 -1
  58. package/dist/process/kill-tree.d.ts +7 -0
  59. package/dist/process/kill-tree.d.ts.map +1 -1
  60. package/dist/process/kill-tree.js +41 -0
  61. package/dist/process/kill-tree.js.map +1 -1
  62. package/dist/provider/errors.d.ts +10 -0
  63. package/dist/provider/errors.d.ts.map +1 -1
  64. package/dist/provider/errors.js +16 -0
  65. package/dist/provider/errors.js.map +1 -1
  66. package/dist/provider/retry.d.ts +27 -0
  67. package/dist/provider/retry.d.ts.map +1 -1
  68. package/dist/provider/retry.js +19 -3
  69. package/dist/provider/retry.js.map +1 -1
  70. package/dist/provider/token-budget.d.ts.map +1 -1
  71. package/dist/provider/token-budget.js +31 -2
  72. package/dist/provider/token-budget.js.map +1 -1
  73. package/dist/public-runtime.d.ts +1 -0
  74. package/dist/public-runtime.d.ts.map +1 -1
  75. package/dist/public-runtime.js +3 -0
  76. package/dist/public-runtime.js.map +1 -1
  77. package/dist/public-tools.d.ts +1 -1
  78. package/dist/public-tools.d.ts.map +1 -1
  79. package/dist/public-tools.js +1 -1
  80. package/dist/public-tools.js.map +1 -1
  81. package/dist/public-types.d.ts +3 -0
  82. package/dist/public-types.d.ts.map +1 -1
  83. package/dist/runtime/jobs/job-id.d.ts +21 -0
  84. package/dist/runtime/jobs/job-id.d.ts.map +1 -0
  85. package/dist/runtime/jobs/job-id.js +66 -0
  86. package/dist/runtime/jobs/job-id.js.map +1 -0
  87. package/dist/runtime/jobs/registry.d.ts +0 -1
  88. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  89. package/dist/runtime/jobs/registry.js +2 -3
  90. package/dist/runtime/jobs/registry.js.map +1 -1
  91. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  92. package/dist/runtime/query/iteration/stream-turn.js +24 -1
  93. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  94. package/dist/runtime/query/prompt.d.ts.map +1 -1
  95. package/dist/runtime/query/prompt.js +5 -1
  96. package/dist/runtime/query/prompt.js.map +1 -1
  97. package/dist/tools/builtins/bash.d.ts +8 -1
  98. package/dist/tools/builtins/bash.d.ts.map +1 -1
  99. package/dist/tools/builtins/bash.js +20 -3
  100. package/dist/tools/builtins/bash.js.map +1 -1
  101. package/dist/tools/builtins/job.d.ts.map +1 -1
  102. package/dist/tools/builtins/job.js +4 -0
  103. package/dist/tools/builtins/job.js.map +1 -1
  104. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  105. package/dist/tools/builtins/wait-for-job.js +4 -0
  106. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  107. package/dist/tools/command-shell.d.ts +55 -0
  108. package/dist/tools/command-shell.d.ts.map +1 -1
  109. package/dist/tools/command-shell.js +174 -13
  110. package/dist/tools/command-shell.js.map +1 -1
  111. package/dist/types/provider/error.d.ts +6 -0
  112. package/dist/types/provider/error.d.ts.map +1 -1
  113. package/dist/types/provider/errors.d.ts.map +1 -1
  114. package/dist/types/provider/errors.js +15 -1
  115. package/dist/types/provider/errors.js.map +1 -1
  116. package/dist/types/provider/stream.d.ts +6 -0
  117. package/dist/types/provider/stream.d.ts.map +1 -1
  118. package/dist/types/session/events.d.ts +5 -0
  119. package/dist/types/session/events.d.ts.map +1 -1
  120. package/dist/types/session/events.js.map +1 -1
  121. package/dist/utils/backoff.d.ts +12 -0
  122. package/dist/utils/backoff.d.ts.map +1 -1
  123. package/dist/utils/backoff.js +15 -0
  124. package/dist/utils/backoff.js.map +1 -1
  125. package/package.json +1 -1
  126. package/src/agents/AGENTS.md +134 -46
  127. package/src/bridge/sse/mapper.ts +1 -0
  128. package/src/execution/code-runtime/types.ts +37 -1
  129. package/src/execution/code-runtime/worker-program.ts +72 -13
  130. package/src/execution/code-runtime/worker.ts +40 -1
  131. package/src/execution/workflow/errors.ts +7 -0
  132. package/src/execution/workflow/index.ts +8 -0
  133. package/src/execution/workflow/journal.ts +46 -0
  134. package/src/execution/workflow/meta.ts +316 -0
  135. package/src/execution/workflow/phases.ts +91 -0
  136. package/src/execution/workflow/prelude.ts +135 -0
  137. package/src/execution/workflow/runtime.ts +786 -0
  138. package/src/execution/workflow/schema.ts +137 -0
  139. package/src/execution/workflow/types.ts +300 -0
  140. package/src/index.ts +4 -0
  141. package/src/plugin/shell-hook.ts +2 -1
  142. package/src/process/kill-tree.ts +43 -0
  143. package/src/provider/errors.ts +18 -0
  144. package/src/provider/retry.ts +53 -3
  145. package/src/provider/token-budget.ts +31 -1
  146. package/src/public-runtime.ts +11 -0
  147. package/src/public-tools.ts +6 -0
  148. package/src/public-types.ts +3 -0
  149. package/src/runtime/jobs/job-id.ts +66 -0
  150. package/src/runtime/jobs/registry.ts +2 -3
  151. package/src/runtime/query/iteration/stream-turn.ts +24 -1
  152. package/src/runtime/query/prompt.ts +5 -1
  153. package/src/tools/builtins/bash.ts +33 -3
  154. package/src/tools/builtins/job.ts +4 -0
  155. package/src/tools/builtins/wait-for-job.ts +4 -0
  156. package/src/tools/command-shell.ts +194 -11
  157. package/src/types/provider/error.ts +6 -0
  158. package/src/types/provider/errors.ts +16 -1
  159. package/src/types/provider/stream.ts +6 -0
  160. package/src/types/session/events.ts +5 -0
  161. package/src/utils/backoff.ts +21 -0
@@ -0,0 +1,7 @@
1
+ /** A script that cannot run: bad `meta`, too large, bad `args`. Nothing started. */
2
+ export class WorkflowScriptError extends Error {
3
+ constructor(message: string) {
4
+ super(message)
5
+ this.name = 'WorkflowScriptError'
6
+ }
7
+ }
@@ -0,0 +1,8 @@
1
+ export { WorkflowScriptError } from './errors.js'
2
+ export { FileWorkflowJournal } from './journal.js'
3
+ export { parseWorkflowMeta } from './meta.js'
4
+ export { summarizeWorkflowPhases } from './phases.js'
5
+ export type { PhaseAgentView, PhaseRecord } from './phases.js'
6
+ export { resolveWorkflowLimits, WORKFLOW_HOST_CALL, WorkflowRuntime } from './runtime.js'
7
+ export type { WorkflowRuntimeOptions } from './runtime.js'
8
+ export type * from './types.js'
@@ -0,0 +1,46 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { appendFile, mkdir, writeFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+
5
+ import type { WorkflowJournal, WorkflowJournalRecord } from './types.js'
6
+
7
+ export function sha256(text: string): string {
8
+ return createHash('sha256').update(text).digest('hex')
9
+ }
10
+
11
+ /** The first `max` UTF-16 units of `text`, never ending inside a surrogate pair. */
12
+ export function preview(text: string, max = 500): string {
13
+ if (text.length <= max) return text
14
+ let end = max
15
+ const last = text.charCodeAt(end - 1)
16
+ if (last >= 0xd800 && last <= 0xdbff) end--
17
+ return `${text.slice(0, end)}…`
18
+ }
19
+
20
+ /**
21
+ * A journal in a directory: `script.js` (the exact script text, mode 0600)
22
+ * and `journal.jsonl` (one JSON record per line, append-only). The caller
23
+ * chooses the directory, normally `<session>/runs/<runId>/workflow/`.
24
+ */
25
+ export class FileWorkflowJournal implements WorkflowJournal {
26
+ private ready: Promise<void> | undefined
27
+
28
+ constructor(private readonly dir: string) {}
29
+
30
+ private ensure(): Promise<void> {
31
+ this.ready ??= mkdir(this.dir, { recursive: true, mode: 0o700 }).then(() => undefined)
32
+ return this.ready
33
+ }
34
+
35
+ async writeScript(source: string): Promise<void> {
36
+ await this.ensure()
37
+ await writeFile(join(this.dir, 'script.js'), source, { mode: 0o600 })
38
+ }
39
+
40
+ async append(record: WorkflowJournalRecord): Promise<void> {
41
+ await this.ensure()
42
+ await appendFile(join(this.dir, 'journal.jsonl'), `${JSON.stringify(record)}\n`, {
43
+ mode: 0o600,
44
+ })
45
+ }
46
+ }
@@ -0,0 +1,316 @@
1
+ import { WorkflowScriptError } from './errors.js'
2
+ import type { WorkflowMeta, WorkflowPhaseMeta } from './types.js'
3
+
4
+ const MAX_META_BYTES = 16 * 1024
5
+ const MAX_DEPTH = 8
6
+ const MAX_PHASES = 50
7
+ const KEYWORD = /export\s+const\s+meta\s*=/y
8
+
9
+ type Literal = string | number | boolean | null | Literal[] | { [key: string]: Literal }
10
+
11
+ /**
12
+ * Reads the `export const meta = {...}` literal WITHOUT running the script.
13
+ *
14
+ * The statement must be the first one in the file (comments and blank lines
15
+ * before it are fine) and its value must be a pure literal: strings, numbers,
16
+ * booleans, null, arrays and objects. Variables, calls, spreads, template
17
+ * substitutions and computed keys are refused with a plain message, because
18
+ * the operator reviews this literal BEFORE the script is allowed to run.
19
+ */
20
+ class LiteralParser {
21
+ pos = 0
22
+
23
+ constructor(private readonly source: string) {}
24
+
25
+ fail(what: string): never {
26
+ const before = this.source.slice(0, this.pos)
27
+ const line = before.split('\n').length
28
+ const column = this.pos - before.lastIndexOf('\n')
29
+ throw new WorkflowScriptError(`${what} (line ${line}, column ${column}).`)
30
+ }
31
+
32
+ skipTrivia(): void {
33
+ const s = this.source
34
+ for (;;) {
35
+ const c = s[this.pos]
36
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '') this.pos++
37
+ else if (c === '/' && s[this.pos + 1] === '/') {
38
+ while (this.pos < s.length && s[this.pos] !== '\n') this.pos++
39
+ } else if (c === '/' && s[this.pos + 1] === '*') {
40
+ const end = s.indexOf('*/', this.pos + 2)
41
+ if (end === -1) this.fail('Unterminated comment')
42
+ this.pos = end + 2
43
+ } else return
44
+ }
45
+ }
46
+
47
+ private describe(): string {
48
+ const snippet = this.source.slice(this.pos, this.pos + 24).split('\n')[0]
49
+ return snippet ? `"${snippet}"` : 'the end of the script'
50
+ }
51
+
52
+ private notLiteral(): never {
53
+ return this.fail(
54
+ `\`meta\` must be a pure literal (no variables, calls, spreads or expressions); found ${this.describe()}`,
55
+ )
56
+ }
57
+
58
+ value(depth: number): Literal {
59
+ if (depth > MAX_DEPTH) this.fail('`meta` is nested too deeply')
60
+ this.skipTrivia()
61
+ const c = this.source[this.pos]
62
+ if (c === '{') return this.object(depth)
63
+ if (c === '[') return this.array(depth)
64
+ if (c === '"' || c === "'") return this.string(c)
65
+ if (c === '`') return this.string('`')
66
+ if (c === '-' || c === '.' || (c !== undefined && c >= '0' && c <= '9')) return this.number()
67
+ for (const [word, value] of [
68
+ ['true', true],
69
+ ['false', false],
70
+ ['null', null],
71
+ ] as const) {
72
+ if (
73
+ this.source.startsWith(word, this.pos) &&
74
+ !/[\w$]/.test(this.source[this.pos + word.length] ?? '')
75
+ ) {
76
+ this.pos += word.length
77
+ return value
78
+ }
79
+ }
80
+ return this.notLiteral()
81
+ }
82
+
83
+ private number(): number {
84
+ const match = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y
85
+ match.lastIndex = this.pos
86
+ const found = match.exec(this.source)
87
+ if (!found) return this.notLiteral()
88
+ this.pos += found[0].length
89
+ if (/[\w$]/.test(this.source[this.pos] ?? '')) return this.notLiteral()
90
+ return Number(found[0])
91
+ }
92
+
93
+ private string(quote: string): string {
94
+ const s = this.source
95
+ this.pos++
96
+ let out = ''
97
+ for (;;) {
98
+ const c = s[this.pos]
99
+ if (c === undefined) return this.fail('Unterminated string in `meta`')
100
+ if (c === quote) {
101
+ this.pos++
102
+ return out
103
+ }
104
+ if (c === '\n' && quote !== '`') return this.fail('Unterminated string in `meta`')
105
+ if (quote === '`' && c === '$' && s[this.pos + 1] === '{') {
106
+ return this.fail('`meta` strings cannot use `${...}` substitutions')
107
+ }
108
+ if (c !== '\\') {
109
+ out += c
110
+ this.pos++
111
+ continue
112
+ }
113
+ const e = s[this.pos + 1]
114
+ this.pos += 2
115
+ const simple: Record<string, string> = {
116
+ n: '\n',
117
+ t: '\t',
118
+ r: '\r',
119
+ b: '\b',
120
+ f: '\f',
121
+ v: '\v',
122
+ '0': '\0',
123
+ '\\': '\\',
124
+ "'": "'",
125
+ '"': '"',
126
+ '`': '`',
127
+ $: '$',
128
+ }
129
+ if (e !== undefined && e in simple) out += simple[e]
130
+ else if (e === '\n') continue
131
+ else if (e === 'u' && s[this.pos] === '{') {
132
+ const end = s.indexOf('}', this.pos)
133
+ const hex = s.slice(this.pos + 1, end)
134
+ if (end === -1 || !/^[0-9a-fA-F]{1,6}$/.test(hex) || Number.parseInt(hex, 16) > 0x10ffff) {
135
+ return this.fail('Bad escape in a `meta` string')
136
+ }
137
+ out += String.fromCodePoint(Number.parseInt(hex, 16))
138
+ this.pos = end + 1
139
+ } else if (e === 'u' || e === 'x') {
140
+ const len = e === 'u' ? 4 : 2
141
+ const hex = s.slice(this.pos, this.pos + len)
142
+ if (!new RegExp(`^[0-9a-fA-F]{${len}}$`).test(hex))
143
+ return this.fail('Bad escape in a `meta` string')
144
+ out += String.fromCharCode(Number.parseInt(hex, 16))
145
+ this.pos += len
146
+ } else return this.fail('Bad escape in a `meta` string')
147
+ }
148
+ }
149
+
150
+ private array(depth: number): Literal[] {
151
+ this.pos++
152
+ const out: Literal[] = []
153
+ for (;;) {
154
+ this.skipTrivia()
155
+ if (this.source[this.pos] === ']') {
156
+ this.pos++
157
+ return out
158
+ }
159
+ out.push(this.value(depth + 1))
160
+ this.skipTrivia()
161
+ if (this.source[this.pos] === ',') this.pos++
162
+ else if (this.source[this.pos] !== ']') return this.notLiteral()
163
+ }
164
+ }
165
+
166
+ private object(depth: number): { [key: string]: Literal } {
167
+ this.pos++
168
+ const out: { [key: string]: Literal } = Object.create(null)
169
+ for (;;) {
170
+ this.skipTrivia()
171
+ const c = this.source[this.pos]
172
+ if (c === '}') {
173
+ this.pos++
174
+ return out
175
+ }
176
+ let key: string
177
+ if (c === '"' || c === "'") key = this.string(c)
178
+ else {
179
+ const ident = /[A-Za-z_$][\w$]*/y
180
+ ident.lastIndex = this.pos
181
+ const found = ident.exec(this.source)
182
+ if (!found) return this.notLiteral()
183
+ key = found[0]
184
+ this.pos += key.length
185
+ }
186
+ if (key === '__proto__') this.fail('`meta` cannot define `__proto__`')
187
+ if (key in out) this.fail(`\`meta\` repeats the key "${key}"`)
188
+ this.skipTrivia()
189
+ if (this.source[this.pos] !== ':') return this.notLiteral()
190
+ this.pos++
191
+ out[key] = this.value(depth + 1)
192
+ this.skipTrivia()
193
+ if (this.source[this.pos] === ',') this.pos++
194
+ else if (this.source[this.pos] !== '}') return this.notLiteral()
195
+ }
196
+ }
197
+ }
198
+
199
+ function text(
200
+ value: Literal | undefined,
201
+ field: string,
202
+ max: number,
203
+ required: boolean,
204
+ ): string | undefined {
205
+ if (value === undefined) {
206
+ if (required) throw new WorkflowScriptError(`\`meta.${field}\` is required.`)
207
+ return undefined
208
+ }
209
+ if (typeof value !== 'string' || value.trim() === '') {
210
+ throw new WorkflowScriptError(`\`meta.${field}\` must be a non-empty string.`)
211
+ }
212
+ if (value.length > max) {
213
+ throw new WorkflowScriptError(`\`meta.${field}\` is longer than ${max} characters.`)
214
+ }
215
+ return value
216
+ }
217
+
218
+ function onlyKeys(
219
+ object: { [key: string]: Literal },
220
+ allowed: readonly string[],
221
+ where: string,
222
+ ): void {
223
+ for (const key of Object.keys(object)) {
224
+ if (!allowed.includes(key)) {
225
+ throw new WorkflowScriptError(
226
+ `\`${where}\` has an unknown key "${key}". Allowed: ${allowed.join(', ')}.`,
227
+ )
228
+ }
229
+ }
230
+ }
231
+
232
+ function isRecord(value: Literal | undefined): value is { [key: string]: Literal } {
233
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
234
+ }
235
+
236
+ export function normalizePhaseTitle(value: string): string {
237
+ return value.replace(/\s+/g, ' ').trim().slice(0, 120)
238
+ }
239
+
240
+ function toMeta(literal: Literal): WorkflowMeta {
241
+ if (!isRecord(literal)) throw new WorkflowScriptError('`meta` must be an object literal.')
242
+ onlyKeys(literal, ['name', 'description', 'whenToUse', 'phases'], 'meta')
243
+ const name = text(literal.name, 'name', 80, true) as string
244
+ const description = text(literal.description, 'description', 500, true) as string
245
+ const whenToUse = text(literal.whenToUse, 'whenToUse', 500, false)
246
+ const phases: WorkflowPhaseMeta[] = []
247
+ if (literal.phases !== undefined) {
248
+ if (!Array.isArray(literal.phases)) {
249
+ throw new WorkflowScriptError('`meta.phases` must be an array of { title, detail?, model? }.')
250
+ }
251
+ if (literal.phases.length > MAX_PHASES) {
252
+ throw new WorkflowScriptError(`\`meta.phases\` lists more than ${MAX_PHASES} phases.`)
253
+ }
254
+ const seen = new Set<string>()
255
+ for (const [i, entry] of literal.phases.entries()) {
256
+ if (!isRecord(entry)) {
257
+ throw new WorkflowScriptError(`\`meta.phases[${i}]\` must be an object with a title.`)
258
+ }
259
+ onlyKeys(entry, ['title', 'detail', 'model'], `meta.phases[${i}]`)
260
+ const raw = text(entry.title, `phases[${i}].title`, 120, true) as string
261
+ const title = normalizePhaseTitle(raw)
262
+ if (seen.has(title)) {
263
+ throw new WorkflowScriptError(`\`meta.phases\` repeats the title "${title}".`)
264
+ }
265
+ seen.add(title)
266
+ const detail = text(entry.detail, `phases[${i}].detail`, 300, false)
267
+ const model = text(entry.model, `phases[${i}].model`, 120, false)
268
+ phases.push({
269
+ title,
270
+ ...(detail !== undefined ? { detail } : {}),
271
+ ...(model !== undefined ? { model } : {}),
272
+ })
273
+ }
274
+ }
275
+ return {
276
+ name,
277
+ description,
278
+ ...(whenToUse !== undefined ? { whenToUse } : {}),
279
+ phases,
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Split a script into its `meta` and its body. The body keeps the script's
285
+ * line breaks (the meta statement is blanked, not removed) so an error line
286
+ * still points at the author's text.
287
+ */
288
+ export function splitWorkflowScript(source: string): { meta: WorkflowMeta; body: string } {
289
+ const parser = new LiteralParser(source)
290
+ parser.skipTrivia()
291
+ KEYWORD.lastIndex = parser.pos
292
+ const found = KEYWORD.exec(source)
293
+ if (!found) {
294
+ throw new WorkflowScriptError(
295
+ 'A workflow script must start with `export const meta = { name, description, phases? }`.',
296
+ )
297
+ }
298
+ const start = parser.pos
299
+ parser.pos = start + found[0].length
300
+ const literal = parser.value(0)
301
+ parser.skipTrivia()
302
+ if (source[parser.pos] === ';') parser.pos++
303
+ if (Buffer.byteLength(source.slice(start, parser.pos)) > MAX_META_BYTES) {
304
+ throw new WorkflowScriptError(`\`meta\` is larger than ${MAX_META_BYTES} bytes.`)
305
+ }
306
+ const meta = toMeta(literal)
307
+ return {
308
+ meta,
309
+ body: source.slice(0, parser.pos).replace(/[^\n]/g, ' ') + source.slice(parser.pos),
310
+ }
311
+ }
312
+
313
+ /** Parse `meta` from a script without executing it. Throws {@link WorkflowScriptError}. */
314
+ export function parseWorkflowMeta(source: string): WorkflowMeta {
315
+ return splitWorkflowScript(source).meta
316
+ }
@@ -0,0 +1,91 @@
1
+ import { normalizePhaseTitle } from './meta.js'
2
+ import type {
3
+ WorkflowAgentState,
4
+ WorkflowPhaseMeta,
5
+ WorkflowPhaseStatus,
6
+ WorkflowPhaseSummary,
7
+ } from './types.js'
8
+
9
+ export interface PhaseRecord {
10
+ readonly index: number
11
+ readonly title: string
12
+ readonly declared: boolean
13
+ }
14
+
15
+ /**
16
+ * Phase titles of one run. A title is the only grouping key: the same title
17
+ * is the same group. Indexes are 1-based first-seen order, declared titles
18
+ * first. `register` reports whether the title was announced just now.
19
+ */
20
+ export class PhaseRegistry {
21
+ private readonly byTitle = new Map<string, PhaseRecord>()
22
+
23
+ register(rawTitle: string, declared = false): { phase: PhaseRecord; isNew: boolean } {
24
+ const title = normalizePhaseTitle(rawTitle)
25
+ const existing = this.byTitle.get(title)
26
+ if (existing) return { phase: existing, isNew: false }
27
+ const phase = { index: this.byTitle.size + 1, title, declared }
28
+ this.byTitle.set(title, phase)
29
+ return { phase, isNew: true }
30
+ }
31
+
32
+ all(): PhaseRecord[] {
33
+ return [...this.byTitle.values()]
34
+ }
35
+ }
36
+
37
+ export interface PhaseAgentView {
38
+ readonly phaseIndex?: number
39
+ readonly state: WorkflowAgentState
40
+ readonly tokens: number
41
+ }
42
+
43
+ function statusOf(total: number, done: number, failed: number): WorkflowPhaseStatus {
44
+ if (total === 0) return 'not-started'
45
+ if (done + failed === total) return failed > 0 ? 'failed' : 'done'
46
+ return 'running'
47
+ }
48
+
49
+ /**
50
+ * Per-phase status from the agents seen so far, in phase order. A phase with
51
+ * no agent yet is `not-started`. When no agent belongs to any phase (and
52
+ * nothing is declared) the run shows one derived group, "Agents"; agents
53
+ * without a phase next to phased ones also land in "Agents", listed last.
54
+ */
55
+ export function summarizeWorkflowPhases(
56
+ declared: readonly WorkflowPhaseMeta[],
57
+ phases: readonly PhaseRecord[],
58
+ agents: readonly PhaseAgentView[],
59
+ ): WorkflowPhaseSummary[] {
60
+ const detail = new Map(declared.map((p) => [normalizePhaseTitle(p.title), p.detail]))
61
+ const summarize = (
62
+ index: number,
63
+ title: string,
64
+ members: readonly PhaseAgentView[],
65
+ extra?: string,
66
+ ): WorkflowPhaseSummary => {
67
+ const done = members.filter((a) => a.state === 'done').length
68
+ const failed = members.filter((a) => a.state === 'error').length
69
+ return {
70
+ index,
71
+ title,
72
+ ...(extra !== undefined ? { detail: extra } : {}),
73
+ status: statusOf(members.length, done, failed),
74
+ agents: members.length,
75
+ done,
76
+ failed,
77
+ tokens: members.reduce((sum, a) => sum + a.tokens, 0),
78
+ }
79
+ }
80
+ const out = phases.map((p) =>
81
+ summarize(
82
+ p.index,
83
+ p.title,
84
+ agents.filter((a) => a.phaseIndex === p.index),
85
+ detail.get(p.title),
86
+ ),
87
+ )
88
+ const ungrouped = agents.filter((a) => a.phaseIndex === undefined)
89
+ if (ungrouped.length > 0) out.push(summarize(0, 'Agents', ungrouped))
90
+ return out
91
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The interpreter-side half of the workflow language: `agent`, `parallel`,
3
+ * `pipeline`, `phase`, `log`, `args` and `budget`.
4
+ *
5
+ * This text runs INSIDE the QuickJS interpreter, ahead of the script body,
6
+ * and is the only place those names are defined. It captures the two bridge
7
+ * natives (`call`, `print`) in its own closure and deletes them from the
8
+ * global object, so the script reaches the host only through `agent()`, and
9
+ * `phase()`/`log()` can only emit well-formed records. None of this is a
10
+ * security boundary: the host re-validates every request and treats every
11
+ * printed line as untrusted text. The boundary is the interpreter itself.
12
+ *
13
+ * Written with String.raw and no backticks or substitutions so no escaping
14
+ * layer can change what the interpreter parses.
15
+ */
16
+ const PRELUDE_SOURCE = String.raw`(function (config) {
17
+ 'use strict';
18
+ var rawCall = globalThis.call;
19
+ var rawPrint = globalThis.print;
20
+ delete globalThis.call;
21
+ delete globalThis.print;
22
+ var stringify = JSON.stringify;
23
+ var spent = { tokensUsed: 0 };
24
+ var cursor = null;
25
+ function emit(record) { rawPrint(stringify(record)); }
26
+ function clean(value, limit) {
27
+ return String(value).replace(/\s+/g, ' ').trim().slice(0, limit);
28
+ }
29
+ function phase(title) {
30
+ var t = clean(title, 120);
31
+ if (!t) throw new TypeError('phase() needs a non-empty title.');
32
+ cursor = t;
33
+ emit({ k: 'phase', title: t });
34
+ }
35
+ function log(message) {
36
+ emit({ k: 'log', message: String(message).slice(0, 2000) });
37
+ }
38
+ function kind(value) {
39
+ if (value === null) return 'null';
40
+ if (value === '') return 'an empty string';
41
+ if (Array.isArray(value)) return 'an array';
42
+ var t = typeof value;
43
+ if (t === 'undefined') return 'undefined';
44
+ if (t === 'object') return typeof value.then === 'function' ? 'a promise' : 'an object';
45
+ return 'a ' + t;
46
+ }
47
+ async function agent(prompt, opts) {
48
+ if (typeof prompt !== 'string' || prompt.length === 0) {
49
+ throw new TypeError('agent(prompt, options?): the prompt must be a non-empty string, got ' + kind(prompt));
50
+ }
51
+ var o = opts == null ? {} : opts;
52
+ if (typeof o !== 'object' || Array.isArray(o)) {
53
+ throw new TypeError('agent(prompt, options?): options must be an object, got ' + kind(o));
54
+ }
55
+ var request = { prompt: prompt, cursor: cursor };
56
+ if (o.label != null) request.label = clean(o.label, 120);
57
+ if (o.phase != null) request.phase = clean(o.phase, 120);
58
+ if (o.schema != null) request.schema = o.schema;
59
+ if (o.agentType != null) request.agentType = clean(o.agentType, 120);
60
+ if (o.model != null) request.model = clean(o.model, 120);
61
+ if (o.effort != null) request.effort = clean(o.effort, 32);
62
+ var reply = await rawCall('agent', request);
63
+ spent.tokensUsed = reply.t;
64
+ return reply.v;
65
+ }
66
+ async function parallel(thunks) {
67
+ if (!Array.isArray(thunks)) {
68
+ throw new TypeError('parallel(thunks): the argument must be an array of functions, got ' + kind(thunks));
69
+ }
70
+ for (var p = 0; p < thunks.length; p++) {
71
+ if (typeof thunks[p] !== 'function') {
72
+ throw new TypeError('parallel(thunks): item ' + (p + 1) + ' must be a function (wrap the work as () => agent(...)), got ' + kind(thunks[p]));
73
+ }
74
+ }
75
+ return Promise.all(thunks.map(function (t) { return t(); }));
76
+ }
77
+ async function pipeline(items) {
78
+ if (!Array.isArray(items)) {
79
+ throw new TypeError('pipeline(items, ...stages): the first argument must be an array, got ' + kind(items));
80
+ }
81
+ var stages = Array.prototype.slice.call(arguments, 1);
82
+ for (var s = 0; s < stages.length; s++) {
83
+ if (typeof stages[s] !== 'function') {
84
+ throw new TypeError('pipeline(items, ...stages): stage ' + (s + 1) + ' must be a function, got ' + kind(stages[s]));
85
+ }
86
+ }
87
+ return Promise.all(items.map(async function (item, index) {
88
+ var value = item;
89
+ for (var i = 0; i < stages.length; i++) {
90
+ value = await stages[i](value, index, item);
91
+ if (value === null || value === undefined) return null;
92
+ }
93
+ return value;
94
+ }));
95
+ }
96
+ var budget = Object.freeze({
97
+ get tokensUsed() { return spent.tokensUsed; },
98
+ get tokenLimit() { return config.tokenLimit === null ? undefined : config.tokenLimit; },
99
+ });
100
+ function install(name, value) {
101
+ Object.defineProperty(globalThis, name, { value: value, writable: false, configurable: false, enumerable: true });
102
+ }
103
+ install('agent', agent);
104
+ install('parallel', parallel);
105
+ install('pipeline', pipeline);
106
+ install('phase', phase);
107
+ install('log', log);
108
+ install('budget', budget);
109
+ install('args', config.args);
110
+ })`
111
+
112
+ /**
113
+ * Lines the prelude occupies in the interpreter source, plus the newline that
114
+ * joins it to the script body: subtract this from an interpreter line number
115
+ * to get the line in the script the author wrote.
116
+ */
117
+ export function preludeLineCount(config: {
118
+ args: unknown
119
+ tokenLimit: number | null
120
+ }): number {
121
+ return buildPrelude(config).split('\n').length
122
+ }
123
+
124
+ /** Source that installs the workflow globals, ready to prepend to a script body. */
125
+ export function buildPrelude(config: {
126
+ args: unknown
127
+ tokenLimit: number | null
128
+ }): string {
129
+ const literal = JSON.stringify(config)
130
+ .split(String.fromCharCode(0x2028))
131
+ .join('\\u2028')
132
+ .split(String.fromCharCode(0x2029))
133
+ .join('\\u2029')
134
+ return `${PRELUDE_SOURCE}(${literal});`
135
+ }