opencode-ultracode 0.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.
@@ -0,0 +1,92 @@
1
+ // Minimal JSON-Schema validator for agent structured output.
2
+ // Supports the subset workflow scripts use: type (object/array/string/number/boolean),
3
+ // properties, required, items, enum, additionalProperties.
4
+
5
+ export class SchemaError extends Error {}
6
+
7
+ export function validateSchema(value: unknown, schema: unknown, path = "$"): void {
8
+ if (!schema || typeof schema !== "object") return
9
+ const s = schema as any
10
+ if (s.type) {
11
+ const types = Array.isArray(s.type) ? s.type : [s.type]
12
+ if (!types.some((t: string) => matchesType(value, t)))
13
+ throw new SchemaError(`${path}: expected type ${types.join("|")}, got ${typeName(value)}`)
14
+ }
15
+ if (s.enum && !s.enum.includes(value))
16
+ throw new SchemaError(`${path}: value not in enum [${s.enum.join(", ")}]`)
17
+ if (s.type === "object" || (typeof value === "object" && value !== null && !Array.isArray(value) && s.properties)) {
18
+ const obj = value as Record<string, unknown>
19
+ for (const req of s.required ?? []) {
20
+ if (!(req in obj)) throw new SchemaError(`${path}: missing required property "${req}"`)
21
+ }
22
+ for (const [key, sub] of Object.entries<any>(s.properties ?? {})) {
23
+ if (key in obj) validateSchema(obj[key], sub, `${path}.${key}`)
24
+ }
25
+ if (s.additionalProperties === false && s.required) {
26
+ const allowed = new Set(s.required)
27
+ for (const k of Object.keys(obj)) if (!allowed.has(k)) {
28
+ // only reject when properties are also defined, to stay permissive
29
+ if (s.properties) throw new SchemaError(`${path}.${k}: additional property not allowed`)
30
+ }
31
+ }
32
+ }
33
+ if (s.type === "array" || Array.isArray(value)) {
34
+ if (Array.isArray(value) && s.items) {
35
+ value.forEach((v, i) => validateSchema(v, s.items, `${path}[${i}]`))
36
+ }
37
+ }
38
+ }
39
+
40
+ function matchesType(value: unknown, type: string): boolean {
41
+ switch (type) {
42
+ case "object":
43
+ return typeof value === "object" && value !== null && !Array.isArray(value)
44
+ case "array":
45
+ return Array.isArray(value)
46
+ case "string":
47
+ return typeof value === "string"
48
+ case "number":
49
+ return typeof value === "number"
50
+ case "integer":
51
+ return Number.isInteger(value)
52
+ case "boolean":
53
+ return typeof value === "boolean"
54
+ case "null":
55
+ return value === null
56
+ default:
57
+ return true
58
+ }
59
+ }
60
+ function typeName(v: unknown): string {
61
+ if (v === null) return "null"
62
+ if (Array.isArray(v)) return "array"
63
+ return typeof v
64
+ }
65
+
66
+ /** Extract the JSON value from a model's text response: last code block or last object/array literal. */
67
+ export function extractJson(text: string): { ok: true; value: unknown } | { ok: false; error: string } {
68
+ const t = text.trim()
69
+ try {
70
+ return { ok: true, value: JSON.parse(t) }
71
+ } catch {}
72
+ // fenced block
73
+ const fences = [...t.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)]
74
+ for (let i = fences.length - 1; i >= 0; i--) {
75
+ const inner = fences[i][1].trim()
76
+ try {
77
+ return { ok: true, value: JSON.parse(inner) }
78
+ } catch {}
79
+ }
80
+ // last balanced object
81
+ for (const open of ["{", "["]) {
82
+ const close = open === "{" ? "}" : "]"
83
+ const start = t.lastIndexOf(open)
84
+ const end = t.lastIndexOf(close)
85
+ if (start !== -1 && end > start) {
86
+ try {
87
+ return { ok: true, value: JSON.parse(t.slice(start, end + 1)) }
88
+ } catch {}
89
+ }
90
+ }
91
+ return { ok: false, error: "no JSON object found in response" }
92
+ }
@@ -0,0 +1,220 @@
1
+ // Workflow script parsing and sandboxed execution context.
2
+ //
3
+ // Scripts are plain JavaScript (NOT TypeScript) that must begin with a pure
4
+ // literal `export const meta = {...}`. The body runs in an async sandbox with
5
+ // the orchestration primitives as parameters. Deterministic-safety:
6
+ // Date.now()/Math.random()/argless new Date() throw, and dangerous globals
7
+ // (process, require, fetch, Bun, ...) are shadowed so they resolve to throwers.
8
+ // This is a determinism sandbox for resume, NOT a security boundary.
9
+
10
+ import type { PhaseDef } from "../shared/state.ts"
11
+
12
+ export interface WorkflowMeta {
13
+ name: string
14
+ description: string
15
+ whenToUse?: string
16
+ phases?: PhaseDef[]
17
+ }
18
+
19
+ export interface ParsedScript {
20
+ meta: WorkflowMeta
21
+ body: string
22
+ }
23
+
24
+ const META_RE = /export\s+const\s+meta\s*=\s*/
25
+
26
+ export function parseScript(script: string): ParsedScript {
27
+ const m = script.match(META_RE)
28
+ if (!m || m.index === undefined) throw new Error("Workflow script must start with `export const meta = { ... }`")
29
+ const start = script.indexOf("{", m.index + m[0].length)
30
+ if (start === -1) throw new Error("Could not find meta object literal")
31
+ const end = findBalanced(script, start)
32
+ const literal = script.slice(start, end + 1)
33
+ // meta must be a pure literal; evaluating an object literal with new Function
34
+ // is safe as long as it contains no expressions — we lint that first.
35
+ if (/[;,\n]\s*function\b|\bnew\s+|\bif\s*\(|\bwhile\s*\(|=>/.test(literal))
36
+ throw new Error("meta must be a pure object literal (no functions, calls, or arrow functions)")
37
+ let meta: WorkflowMeta
38
+ try {
39
+ meta = new Function(`"use strict"; return (${literal})`)() as WorkflowMeta
40
+ } catch (e: any) {
41
+ throw new Error(`Invalid meta literal: ${e?.message ?? e}`)
42
+ }
43
+ if (!meta || typeof meta !== "object") throw new Error("meta must be an object")
44
+ if (!meta.name || typeof meta.name !== "string") throw new Error("meta.name is required")
45
+ if (!meta.description || typeof meta.description !== "string")
46
+ throw new Error("meta.description is required (one line, shown in the approval dialog)")
47
+ for (const p of meta.phases ?? []) {
48
+ if (!p.title) throw new Error("each meta.phases entry needs a title")
49
+ }
50
+ const body = (script.slice(0, start) + " ".repeat(end - start + 1) + script.slice(end + 1)).replace(META_RE, "")
51
+ return { meta, body }
52
+ }
53
+
54
+ function findBalanced(s: string, openIdx: number): number {
55
+ let depth = 0
56
+ let inStr: string | null = null
57
+ for (let i = openIdx; i < s.length; i++) {
58
+ const c = s[i]
59
+ if (inStr) {
60
+ if (c === "\\") i++
61
+ else if (c === inStr) inStr = null
62
+ continue
63
+ }
64
+ if (c === '"' || c === "'" || c === "`") inStr = c
65
+ else if (c === "{" || c === "[" || c === "(") depth++
66
+ else if (c === "}" || c === "]" || c === ")") {
67
+ depth--
68
+ if (depth === 0) return i
69
+ }
70
+ }
71
+ throw new Error("Unbalanced meta literal")
72
+ }
73
+
74
+ // --- sandbox ----------------------------------------------------------------
75
+
76
+ export interface Primitives {
77
+ agent: (prompt: string, opts?: AgentOpts) => Promise<any>
78
+ parallel: (thunks: Array<() => Promise<any>>) => Promise<any[]>
79
+ pipeline: <T, R>(items: T[], ...stages: Array<(prev: any, item: T, index: number) => Promise<R> | R>) => Promise<any[]>
80
+ phase: (title: string) => void
81
+ log: (message: string) => void
82
+ args: any
83
+ budget: Budget
84
+ }
85
+
86
+ export interface AgentOpts {
87
+ label?: string
88
+ phase?: string
89
+ schema?: object
90
+ model?: string
91
+ effort?: string
92
+ isolation?: string
93
+ agentType?: string
94
+ }
95
+
96
+ export interface Budget {
97
+ total: number | null
98
+ spent(): number
99
+ remaining(): number
100
+ }
101
+
102
+ function throwing(name: string): any {
103
+ const f: any = () => {
104
+ throw new Error(`Access to global "${name}" is not available in workflow scripts; use log() for output`)
105
+ }
106
+ return new Proxy(f, {
107
+ get(_t, prop) {
108
+ if (prop === Symbol.toPrimitive || prop === "then") return undefined
109
+ throw new Error(`Access to "${name}.${String(prop)}" is not available in workflow scripts`)
110
+ },
111
+ apply: () => {
112
+ throw new Error(`Access to global "${name}" is not available in workflow scripts`)
113
+ },
114
+ })
115
+ }
116
+
117
+ /** Deterministic Date: no-arg new Date() and Date.now() throw. */
118
+ function makeSafeDate(): any {
119
+ class SafeDate {
120
+ getTime = () => 0
121
+ valueOf = () => 0
122
+ toString = () => ""
123
+ toISOString = () => ""
124
+ constructor(...args: any[]) {
125
+ if (args.length === 0)
126
+ throw new Error("new Date() without arguments is not available in workflow scripts (breaks resume); pass a timestamp via args")
127
+ // delegate to the real Date via a hidden reference
128
+ const D = makeSafeDate as any;
129
+ const real = new D.__impl(...args)
130
+ for (const k of Object.keys(real)) (this as any)[k] = (real as any)[k]
131
+ this.getTime = real.getTime.bind(real)
132
+ this.valueOf = real.valueOf.bind(real)
133
+ this.toString = real.toString.bind(real)
134
+ this.toISOString = real.toISOString.bind(real)
135
+ }
136
+ static now() {
137
+ throw new Error("Date.now() is not available in workflow scripts (breaks resume); pass a timestamp via args")
138
+ }
139
+ static parse(...a: any[]) {
140
+ const D = makeSafeDate as any
141
+ return D.__impl.parse(...a)
142
+ }
143
+ static UTC(...a: any[]) {
144
+ const D = makeSafeDate as any
145
+ return D.__impl.UTC(...a)
146
+ }
147
+ }
148
+ ;(makeSafeDate as any).__impl = Date
149
+ return SafeDate
150
+ }
151
+
152
+ /** Math minus random. */
153
+ function makeSafeMath(): any {
154
+ const real = Object.create(Math)
155
+ return new Proxy(real, {
156
+ get(t, prop) {
157
+ if (prop === "random")
158
+ return () => {
159
+ throw new Error("Math.random() is not available in workflow scripts (breaks resume); vary prompts/labels by index")
160
+ }
161
+ const v = (t as any)[prop]
162
+ return typeof v === "function" ? v.bind(t) : v
163
+ },
164
+ })
165
+ }
166
+
167
+ const SHADOWED = [
168
+ "require",
169
+ "process",
170
+ "Bun",
171
+ "fetch",
172
+ "WebSocket",
173
+ "EventSource",
174
+ "crypto",
175
+ "globalThis",
176
+ "self",
177
+ "window",
178
+ "global",
179
+ "Buffer",
180
+ "child_process",
181
+ "fs",
182
+ "os",
183
+ "module",
184
+ "exports",
185
+ "__dirname",
186
+ "__filename",
187
+ ] as const
188
+
189
+ /**
190
+ * Build the sandboxed script function. The body runs in an async IIFE with the
191
+ * primitives in scope; dangerous globals are shadowed by the proxy throwers.
192
+ */
193
+ export function buildScriptFunction(prims: Primitives, body: string): () => Promise<any> {
194
+ const safeDate = makeSafeDate()
195
+ const safeMath = makeSafeMath()
196
+ const shadows = SHADOWED.map((n) => throwing(n))
197
+ const fn = new Function(
198
+ "agent",
199
+ "parallel",
200
+ "pipeline",
201
+ "phase",
202
+ "log",
203
+ "args",
204
+ "budget",
205
+ "Date",
206
+ "Math",
207
+ "console",
208
+ ...SHADOWED,
209
+ `"use strict"; return (async () => {\n${body}\n})()`,
210
+ )
211
+ const consoleShadow = new Proxy({} as Record<string | symbol, any>, {
212
+ get() {
213
+ return (..._a: any[]) => {
214
+ throw new Error("console is not available in workflow scripts; use log()")
215
+ }
216
+ },
217
+ })
218
+ return () =>
219
+ fn(prims.agent, prims.parallel, prims.pipeline, prims.phase, prims.log, prims.args, prims.budget, safeDate, safeMath, consoleShadow, ...shadows)
220
+ }
@@ -0,0 +1,321 @@
1
+ // Standalone engine test: runs workflow scripts against a mocked opencode client.
2
+ // Usage: node src/runtime/selftest.ts
3
+
4
+ import { mkdtempSync, readFileSync, existsSync, writeFileSync } from "node:fs"
5
+ import { tmpdir } from "node:os"
6
+ import { join } from "node:path"
7
+ import assert from "node:assert"
8
+ import { generateRunId, loadPriorRun, RunEngine } from "./engine.ts"
9
+ import { parseScript } from "./script.ts"
10
+
11
+ const tmp = mkdtempSync(join(tmpdir(), "wf-selftest-"))
12
+ const opencodeDir = join(tmp, ".opencode")
13
+ const runsRoot = join(tmp, "runs")
14
+
15
+ let sessionSeq = 0
16
+ let promptCount = 0
17
+ const seenSessions: string[] = []
18
+
19
+ const fakeClient = {
20
+ session: {
21
+ async create(args: any) {
22
+ const id = `ses_fake_${++sessionSeq}`
23
+ seenSessions.push(id)
24
+ console.log(` [mock] session.create title=${args.body?.title} -> ${id}`)
25
+ return { id, parentID: args.body?.parentID }
26
+ },
27
+ async prompt(args: any) {
28
+ promptCount++
29
+ const id = args.path.id
30
+ const text = String(args.body?.parts?.[0]?.text ?? "")
31
+ console.log(` [mock] session.prompt ${id} model=${args.body?.model ? JSON.stringify(args.body.model) : "(inherit)"} chars=${text.length}`)
32
+ // emulate the model: if the task asks for JSON tips, return JSON; synthesis returns JSON report
33
+ if (/\bJSON\b/i.test(text)) {
34
+ if (/synthes/i.test(text)) {
35
+ const extracted = text.match(/\[.*\]/s)?.[0]
36
+ let parsed: any[] = []
37
+ try {
38
+ parsed = JSON.parse(extracted ?? "[]")
39
+ } catch {}
40
+ const best = parsed.slice(0, 3).map((p: any) => p?.tip ?? "")
41
+ return {
42
+ info: assistantMsg(id),
43
+ parts: [{ type: "text", id: "p1", sessionID: id, messageID: "m1", text: JSON.stringify({ report: { count: best.length, best }, }) }],
44
+ }
45
+ }
46
+ const angleMatch = text.match(/about (\w+) multi-agent workflows/i)
47
+ const angle = angleMatch?.[1] ?? "general"
48
+ return {
49
+ info: assistantMsg(id),
50
+ parts: [{ type: "text", id: "p1", sessionID: id, messageID: "m1", text: JSON.stringify({ angle: `${angle} multi-agent workflows: mock tip`, tip: `A mock tip about ${angle}.` }) }],
51
+ }
52
+ }
53
+ return {
54
+ info: assistantMsg(id),
55
+ parts: [{ type: "text", id: "p1", sessionID: id, messageID: "m1", text: "plain text answer" }],
56
+ }
57
+ },
58
+ async abort(args: any) {
59
+ console.log(` [mock] session.abort ${args.path.id}`)
60
+ return true
61
+ },
62
+ },
63
+ }
64
+
65
+ function assistantMsg(sessionID = "x") {
66
+ return {
67
+ id: `msg_${Math.random().toString(36).slice(2)}`,
68
+ sessionID,
69
+ role: "assistant",
70
+ time: { created: Date.now(), completed: Date.now() },
71
+ parentID: "p",
72
+ modelID: "mock-model",
73
+ providerID: "mock",
74
+ mode: "workflow",
75
+ path: { cwd: "/", root: "/" },
76
+ cost: 0.001,
77
+ tokens: { input: 10_000, output: 3_600, reasoning: 0, cache: { read: 20_000, write: 500 } },
78
+ }
79
+ }
80
+
81
+ const DEMO = `
82
+ export const meta = {
83
+ name: "demo-fanout",
84
+ description: "Demo: 3 agents write tips on multi-agent workflows, 1 agent synthesizes",
85
+ phases: [
86
+ { title: "Generate", detail: "3 agents, one tip each" },
87
+ { title: "Synthesize", detail: "merge the tips into the final set" },
88
+ ],
89
+ }
90
+
91
+ const ANGLES = ["design", "execution", "debugging"]
92
+
93
+ phase("Generate")
94
+ const results = await parallel(ANGLES.map((a) => () =>
95
+ agent(
96
+ \`Write one concise tip (2-3 sentences) about \${a.toUpperCase()} multi-agent workflows: concurrency limits, token budget, and isolation. Respond with JSON.\`,
97
+ {
98
+ label: \`tip:\${a}\`,
99
+ schema: { type: "object", properties: { angle: { type: "string" }, tip: { type: "string" } }, required: ["angle", "tip"] },
100
+ },
101
+ ),
102
+ ))
103
+ const tips = results.filter(Boolean)
104
+ log(\`\${tips.length}/\${ANGLES.length} tips generated\`)
105
+
106
+ phase("Synthesize")
107
+ const final = await agent(
108
+ \`Synthesize these tips into the single best set (pick up to 3). Respond with JSON. Tips: \${JSON.stringify(tips, null, 2)}\`,
109
+ {
110
+ label: "tip:synthesis",
111
+ schema: { type: "object", properties: { report: { type: "object" } }, required: ["report"] },
112
+ },
113
+ )
114
+ return { tips, final }
115
+ `
116
+
117
+ async function main() {
118
+ // ---- test 1: meta parsing ------------------------------------------------
119
+ console.log("test 1: parseScript meta + body")
120
+ const parsed = parseScript(DEMO)
121
+ assert.equal(parsed.meta.name, "demo-fanout")
122
+ assert.equal(parsed.meta.phases?.length, 2)
123
+ assert.ok(!/^export\s+const\s+meta/m.test(parsed.body.trimStart()), "body should not retain meta")
124
+ assert.ok(parsed.body.includes("parallel("))
125
+ console.log(" ok: meta parsed, body clean")
126
+
127
+ const bad = () => parseScript("const meta = { name: 'x' };\nagent('hi')")
128
+ assert.throws(bad, /must start with/)
129
+ const impure = `export const meta = { name: "x", description: f() };\nphase("a")`
130
+ // f() is a call — our lint only bans functions/new/if/while/arrow; a plain call() would pass lint
131
+ // and blow up at eval time: still an error either way
132
+ assert.throws(() => parseScript(impure), /meta|moust|literal|name|description/i)
133
+ console.log(" ok: invalid scripts rejected")
134
+
135
+ // ---- test 2: full demo run ----------------------------------------------
136
+ console.log("test 2: full demo-fanout run (4 agents)")
137
+ const runId = generateRunId()
138
+ const engine = new RunEngine(
139
+ { client: fakeClient as any, opencodeDir, runsRoot, mainSessionID: "ses_main", defaultModel: "mock/sonnet", availableModels: new Set(["mock/sonnet", "mock/haiku"]), runArgs: { extra: 1 } },
140
+ runId,
141
+ )
142
+ const t0 = Date.now()
143
+ const res = await engine.run({ script: DEMO })
144
+ const dt = Date.now() - t0
145
+ console.log(` result: status=${res.status} in ${dt}ms`)
146
+ assert.equal(res.status, "completed", `expected completed, error=${res.error}`)
147
+ assert.ok(res.result, "expected a result payload")
148
+ const result = JSON.parse(res.result!)
149
+ assert.ok(Array.isArray(result.tips) && result.tips.length === 3, "expected 3 tips")
150
+ assert.ok(result.final?.report?.best?.length <= 3, "expected synthesized report")
151
+
152
+ // state file
153
+ const state = JSON.parse(readFileSync(join(runsRoot, runId, "state.json"), "utf8"))
154
+ assert.equal(state.status, "completed")
155
+ assert.equal(state.agentCount, 4)
156
+ assert.equal(state.agentDone, 4)
157
+ assert.equal(state.phases.length, 2)
158
+ assert.equal(state.phases[0].title, "Generate")
159
+ assert.equal(state.phases[0].agentIds.length, 3)
160
+ assert.equal(state.phases[1].agentIds.length, 1)
161
+ const labels = state.agentOrder.map((id: string) => state.agents[id].label)
162
+ assert.deepEqual(labels, ["tip:design", "tip:execution", "tip:debugging", "tip:synthesis"])
163
+ const synth = state.agents[state.agentOrder[3]]
164
+ assert.equal(synth.model, "mock/sonnet", "agent should inherit session model")
165
+ assert.ok(synth.tokens > 0, "tokens tracked")
166
+ // mock message: input 10k + cache read 20k + cache write 500 = 30.5k context per call
167
+ assert.equal(synth.contextTokens, 30_500, "context = prompt size of the latest call")
168
+ assert.ok(synth.tokens >= synth.contextTokens + 3_600, "billed includes output on top of context")
169
+ assert.ok(synth.outcome, "outcome stored")
170
+ assert.ok(state.totalTokens > 0)
171
+ assert.equal(
172
+ state.totalContextTokens,
173
+ state.agentOrder.reduce((n: number, id: string) => n + state.agents[id].contextTokens, 0),
174
+ "run context = sum of agent contexts",
175
+ )
176
+ console.log(` ok: state.json (4 agents, 2 phases, billed=${state.totalTokens}, context=${state.totalContextTokens})`)
177
+
178
+ // journal
179
+ const journal = readFileSync(join(runsRoot, runId, "journal.jsonl"), "utf8").trim().split("\n")
180
+ assert.ok(journal.length >= 9, `journal lines = ${journal.length}`)
181
+ const kinds = journal.map((l) => JSON.parse(l).type)
182
+ assert.ok(kinds.includes("run-start") && kinds.includes("run-end"))
183
+ assert.equal(kinds.filter((k: string) => k === "agent-start").length, 4)
184
+ assert.equal(kinds.filter((k: string) => k === "agent-done").length, 4)
185
+ console.log(" ok: journal (run-start, 4x agent-start/done, run-end)")
186
+
187
+ // script.js persisted
188
+ assert.ok(existsSync(join(runsRoot, runId, "script.js")), "script.js persisted")
189
+ console.log(" ok: script.js persisted")
190
+
191
+ // ---- test 3: deterministic sandbox ---------------------------------------
192
+ console.log("test 3: sandbox blocks Date.now / Math.random / fetch / process")
193
+ const S1 = `
194
+ export const meta = { name: "t3", description: "sandbox", phases: [{ title: "A" }] }
195
+ phase("A")
196
+ try { Date.now() } catch (e) { log("date-now-blocked:" + (String(e.message).includes("not available"))) }
197
+ try { Math.random() } catch (e) { log("math-random-blocked:" + (String(e.message).includes("not available"))) }
198
+ try { fetch("http://x") } catch (e) { log("fetch-blocked:" + (String(e.message).includes("not available"))) }
199
+ try { process.exit } catch (e) { log("process-blocked:" + true) }
200
+ await agent("say hi", { label: "t3a" })
201
+ return "done"
202
+ `
203
+ const rid3 = generateRunId()
204
+ const e3 = new RunEngine({ client: fakeClient as any, opencodeDir, runsRoot, mainSessionID: "ses_main", availableModels: new Set() }, rid3)
205
+ const r3 = await e3.run({ script: S1 })
206
+ assert.equal(r3.status, "completed")
207
+ const s3 = JSON.parse(readFileSync(join(runsRoot, rid3, "state.json"), "utf8"))
208
+ const logs = s3.logs.map((l: any) => l.message).join(" | ")
209
+ console.log(` logs: ${logs}`)
210
+ assert.ok(logs.includes("date-now-blocked:true"))
211
+ assert.ok(logs.includes("math-random-blocked:true"))
212
+ assert.ok(logs.includes("fetch-blocked:true"))
213
+ assert.ok(logs.includes("process-blocked:true"))
214
+ console.log(" ok: sandbox blocks verified")
215
+
216
+ // Date with args still works
217
+ const S2 = `
218
+ export const meta = { name: "t3b", description: "date-with-args", phases: [{ title: "A" }] }
219
+ phase("A")
220
+ log("date-ok:" + (new Date(1700000000000).getTime() === 1700000000000))
221
+ return 42
222
+ `
223
+ const rid3b = generateRunId()
224
+ const e3b = new RunEngine({ client: fakeClient as any, opencodeDir, runsRoot, mainSessionID: "ses_main", availableModels: new Set() }, rid3b)
225
+ const r3b = await e3b.run({ script: S2 })
226
+ assert.equal(r3b.status, "completed")
227
+ const s3b = JSON.parse(readFileSync(join(runsRoot, rid3b, "state.json"), "utf8"))
228
+ assert.ok(s3b.logs.some((l: any) => l.message === "date-ok:true"))
229
+ assert.equal(r3b.result, "42")
230
+ console.log(" ok: new Date(ts) + return values work")
231
+
232
+ // ---- test 4: pipeline stages (no barrier) + error -> null -----------------
233
+ console.log("test 4: pipeline + error handling")
234
+ const S4 = `
235
+ export const meta = { name: "t4", description: "pipeline", phases: [{ title: "P" }] }
236
+ phase("P")
237
+ const items = ["a", "b", "c"]
238
+ const out = await pipeline(items,
239
+ (prev, item) => agent("work on " + item, { label: "work:" + item }),
240
+ (prev, item, i) => { if (item === "b") throw new Error("boom"); return prev + "/" + i },
241
+ )
242
+ const bad = await parallel([() => agent("fine"), () => Promise.reject(new Error("nope"))])
243
+ log("pipeline:" + out.map(x => x ?? "null").join(","))
244
+ log("parallel-nulls:" + bad.filter(Boolean).length + "/" + bad.length)
245
+ return out
246
+ `
247
+ const rid4 = generateRunId()
248
+ const e4 = new RunEngine({ client: fakeClient as any, opencodeDir, runsRoot, mainSessionID: "ses_main", availableModels: new Set() }, rid4)
249
+ const r4 = await e4.run({ script: S4 })
250
+ assert.equal(r4.status, "completed")
251
+ const s4 = JSON.parse(readFileSync(join(runsRoot, rid4, "state.json"), "utf8"))
252
+ const l4 = s4.logs.map((l: any) => l.message).join(" | ")
253
+ console.log(` logs: ${l4}`)
254
+ const pm = l4.match(/pipeline:(.*) \| parallel-nulls/)
255
+ assert.ok(pm, `no pipeline line: ${l4}`)
256
+ const line = pm[1] // "<item0>,<item1>,<item2>" where item1 is null
257
+ const mid = line.indexOf(",null,")
258
+ assert.ok(mid > 0, `expected a null middle item: ${line}`)
259
+ assert.ok(line.slice(0, mid).endsWith("/0"), `item0 should end with /0: ${line.slice(0, mid)}`)
260
+ assert.ok(line.endsWith("/2"), `item2 should end with /2: ${line}`)
261
+ assert.ok(line.slice(mid + 6).startsWith("{"), `item2 should be a result object: ${line}`)
262
+ assert.ok(l4.includes("parallel-nulls:1/2"))
263
+ const failed = s4.agentOrder.map((id: string) => s4.agents[id]).filter((a: any) => a.status === "failed")
264
+ assert.equal(failed.length, 0, "pipeline stage throw should drop item, not fail agent (agent itself succeeded)")
265
+ console.log(" ok: pipeline stages, item drop on stage error, parallel null on reject")
266
+
267
+ // ---- test 5: resume after the engine died ----------------------------------
268
+ console.log("test 5: resume a stopped run (completed agents replay from the journal)")
269
+ {
270
+ const rid = generateRunId()
271
+ const deps = { client: fakeClient as any, opencodeDir, runsRoot, mainSessionID: "ses_main", defaultModel: "mock/sonnet", availableModels: new Set(["mock/sonnet"]) }
272
+ const e1 = new RunEngine(deps, rid)
273
+ // make the synthesis agent die mid-run: stop the run once the Generate phase is done
274
+ const origPrompt = fakeClient.session.prompt
275
+ let calls = 0
276
+ fakeClient.session.prompt = async (args: any) => {
277
+ calls++
278
+ if (calls === 4) {
279
+ e1.shutdown("opencode exited while the workflow was running")
280
+ throw new Error("connection closed")
281
+ }
282
+ return origPrompt(args)
283
+ }
284
+ const r1 = await e1.run({ script: DEMO, args: { extra: 7 } })
285
+ fakeClient.session.prompt = origPrompt
286
+ assert.equal(r1.status, "stopped", `expected stopped, got ${r1.status} (${r1.error})`)
287
+ const s1: any = JSON.parse(readFileSync(join(runsRoot, rid, "state.json"), "utf8"))
288
+ assert.equal(s1.mainSessionID, "ses_main")
289
+ assert.deepEqual(s1.args, { extra: 7 })
290
+ const done1 = Object.values(s1.agents).filter((a: any) => a.status === "completed").length
291
+ assert.equal(done1, 3, `3 tip agents should have completed before the stop (got ${done1})`)
292
+
293
+ const prior = loadPriorRun(runsRoot, rid)
294
+ assert.ok(prior, "prior run loads")
295
+ assert.equal(prior!.replayable, 3)
296
+ const before = promptCount
297
+ const e2 = new RunEngine(deps, rid)
298
+ const r2 = await e2.run({ resume: prior! })
299
+ assert.equal(r2.status, "completed", `resumed run should complete, error=${r2.error}`)
300
+ assert.equal(promptCount - before, 1, "only the synthesis agent should call the model again")
301
+ const s2: any = JSON.parse(readFileSync(join(runsRoot, rid, "state.json"), "utf8"))
302
+ const replayed = Object.values(s2.agents).filter((a: any) => a.replayed).length
303
+ assert.equal(replayed, 3, "3 agents replayed")
304
+ assert.equal(s2.agentCount, 4)
305
+ assert.equal(s2.resumeCount, 1)
306
+ assert.equal(s2.startedAt, s1.startedAt, "original start time kept")
307
+ assert.ok(s2.logs.some((l: any) => /paused|stopped|exited/.test(l.message)), "prior logs carried over")
308
+ assert.ok(s2.logs.some((l: any) => /^resumed \(3 completed agents replay/.test(l.message)), "resume log line")
309
+ const final = JSON.parse(r2.result!)
310
+ assert.equal(final.tips.length, 3, "replayed results feed the rest of the script")
311
+ assert.ok(!s2.error, "error cleared on successful resume")
312
+ console.log(` ok: resume replayed ${replayed} agents, re-ran 1, completed`)
313
+ }
314
+
315
+ console.log(`\nALL TESTS PASSED (${Date.now() - t0}ms total, tmp=${tmp})`)
316
+ }
317
+
318
+ main().catch((e) => {
319
+ console.error("\nSELFTEST FAILED:", e)
320
+ process.exit(1)
321
+ })