@studio-foundation/engine 0.4.0-beta → 0.6.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.
- package/README.md +1 -1
- package/dist/__tests__/engine.script-stage.test.js +13 -32
- package/dist/__tests__/engine.script-stage.test.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +139 -1
- package/dist/engine.js.map +1 -1
- package/dist/events.d.ts +50 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/call-input.d.ts +4 -0
- package/dist/pipeline/call-input.d.ts.map +1 -0
- package/dist/pipeline/call-input.js +28 -0
- package/dist/pipeline/call-input.js.map +1 -0
- package/dist/pipeline/call-orchestrator.d.ts +21 -0
- package/dist/pipeline/call-orchestrator.d.ts.map +1 -0
- package/dist/pipeline/call-orchestrator.js +127 -0
- package/dist/pipeline/call-orchestrator.js.map +1 -0
- package/dist/pipeline/condition-evaluator.d.ts +12 -0
- package/dist/pipeline/condition-evaluator.d.ts.map +1 -1
- package/dist/pipeline/condition-evaluator.js +27 -9
- package/dist/pipeline/condition-evaluator.js.map +1 -1
- package/dist/pipeline/context-propagation.d.ts +1 -0
- package/dist/pipeline/context-propagation.d.ts.map +1 -1
- package/dist/pipeline/context-propagation.js +18 -0
- package/dist/pipeline/context-propagation.js.map +1 -1
- package/dist/pipeline/contract-loader.d.ts.map +1 -1
- package/dist/pipeline/contract-loader.js +67 -0
- package/dist/pipeline/contract-loader.js.map +1 -1
- package/dist/pipeline/external-validator.d.ts +3 -0
- package/dist/pipeline/external-validator.d.ts.map +1 -0
- package/dist/pipeline/external-validator.js +65 -0
- package/dist/pipeline/external-validator.js.map +1 -0
- package/dist/pipeline/external-validator.test.d.ts +2 -0
- package/dist/pipeline/external-validator.test.d.ts.map +1 -0
- package/dist/pipeline/external-validator.test.js +40 -0
- package/dist/pipeline/external-validator.test.js.map +1 -0
- package/dist/pipeline/loader.d.ts.map +1 -1
- package/dist/pipeline/loader.js +158 -1
- package/dist/pipeline/loader.js.map +1 -1
- package/dist/pipeline/loader.test.js +30 -0
- package/dist/pipeline/loader.test.js.map +1 -1
- package/dist/pipeline/map-input.d.ts +13 -0
- package/dist/pipeline/map-input.d.ts.map +1 -0
- package/dist/pipeline/map-input.js +80 -0
- package/dist/pipeline/map-input.js.map +1 -0
- package/dist/pipeline/map-item-cache.d.ts +47 -0
- package/dist/pipeline/map-item-cache.d.ts.map +1 -0
- package/dist/pipeline/map-item-cache.js +0 -0
- package/dist/pipeline/map-item-cache.js.map +1 -0
- package/dist/pipeline/map-orchestrator.d.ts +42 -0
- package/dist/pipeline/map-orchestrator.d.ts.map +1 -0
- package/dist/pipeline/map-orchestrator.js +283 -0
- package/dist/pipeline/map-orchestrator.js.map +1 -0
- package/dist/pipeline/output-checker.d.ts +3 -0
- package/dist/pipeline/output-checker.d.ts.map +1 -0
- package/dist/pipeline/output-checker.js +40 -0
- package/dist/pipeline/output-checker.js.map +1 -0
- package/dist/pipeline/output-checker.test.d.ts +2 -0
- package/dist/pipeline/output-checker.test.d.ts.map +1 -0
- package/dist/pipeline/output-checker.test.js +62 -0
- package/dist/pipeline/output-checker.test.js.map +1 -0
- package/dist/pipeline/stage-executor.d.ts.map +1 -1
- package/dist/pipeline/stage-executor.js +48 -9
- package/dist/pipeline/stage-executor.js.map +1 -1
- package/dist/pipeline/stage-resolver.js +1 -1
- package/dist/pipeline/stage-resolver.js.map +1 -1
- package/dist/pipeline/strict-fields.d.ts +20 -0
- package/dist/pipeline/strict-fields.d.ts.map +1 -0
- package/dist/pipeline/strict-fields.js +62 -0
- package/dist/pipeline/strict-fields.js.map +1 -0
- package/dist/pipeline/task-input.d.ts +14 -0
- package/dist/pipeline/task-input.d.ts.map +1 -0
- package/dist/pipeline/task-input.js +22 -0
- package/dist/pipeline/task-input.js.map +1 -0
- package/dist/pipeline/task-input.test.d.ts +2 -0
- package/dist/pipeline/task-input.test.d.ts.map +1 -0
- package/dist/pipeline/task-input.test.js +33 -0
- package/dist/pipeline/task-input.test.js.map +1 -0
- package/dist/pipeline/template-interpolation.d.ts +9 -0
- package/dist/pipeline/template-interpolation.d.ts.map +1 -0
- package/dist/pipeline/template-interpolation.js +33 -0
- package/dist/pipeline/template-interpolation.js.map +1 -0
- package/dist/state/status-derivation.js +3 -3
- package/dist/state/status-derivation.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// MapOrchestrator — executes a fan-out (map) stage.
|
|
2
|
+
//
|
|
3
|
+
// A map stage runs a sub-pipeline once per item of a list and collects the
|
|
4
|
+
// structured outputs. It replaces the "shell `studio run <pipeline>` per item
|
|
5
|
+
// + parse stdout + scrape the run log" glue: child runs are spawned in-process
|
|
6
|
+
// through the engine's RunSpawner and each returns its last-stage output
|
|
7
|
+
// directly — no scraping, no tempfiles.
|
|
8
|
+
//
|
|
9
|
+
// Concurrency is bounded by `concurrency` (default 1 = sequential). The
|
|
10
|
+
// per-item failure policy is `on_item_failure`:
|
|
11
|
+
// - fail-fast (default): stop launching new items on the first failure; the
|
|
12
|
+
// stage fails. In-flight items are allowed to finish.
|
|
13
|
+
// - collect-all: run every item regardless; the stage succeeds as long as at
|
|
14
|
+
// least one item succeeded (or the list was empty). Per-item failures are
|
|
15
|
+
// surfaced in the output, never fatal — the pipeline keeps going.
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { resolveContextPath, evaluateCondition } from './condition-evaluator.js';
|
|
18
|
+
import { buildItemInput, mapItemLabel } from './map-input.js';
|
|
19
|
+
import { hashItemInput } from './map-item-cache.js';
|
|
20
|
+
function errorMessage(err) {
|
|
21
|
+
return err instanceof Error ? err.message : String(err);
|
|
22
|
+
}
|
|
23
|
+
export class MapOrchestrator {
|
|
24
|
+
config;
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
async run(map, context, stageIndex, totalStages, runId, depth, pipelineName, signal) {
|
|
29
|
+
const startedAt = new Date().toISOString();
|
|
30
|
+
const stageRun = {
|
|
31
|
+
id: randomUUID(),
|
|
32
|
+
stage_name: map.map,
|
|
33
|
+
status: 'running',
|
|
34
|
+
started_at: startedAt,
|
|
35
|
+
tasks: [],
|
|
36
|
+
};
|
|
37
|
+
const finish = (status, output, skippedReason) => {
|
|
38
|
+
stageRun.status = status;
|
|
39
|
+
stageRun.completed_at = new Date().toISOString();
|
|
40
|
+
if (output !== undefined)
|
|
41
|
+
stageRun.output = output;
|
|
42
|
+
if (skippedReason)
|
|
43
|
+
stageRun.skipped_reason = skippedReason;
|
|
44
|
+
this.config.events?.onStageComplete?.({
|
|
45
|
+
stage_name: map.map,
|
|
46
|
+
stage_index: stageIndex,
|
|
47
|
+
total_stages: totalStages,
|
|
48
|
+
status,
|
|
49
|
+
attempts: 1,
|
|
50
|
+
duration_ms: Date.now() - new Date(startedAt).getTime(),
|
|
51
|
+
...(output ? { output } : {}),
|
|
52
|
+
...(skippedReason ? { skipped_reason: skippedReason } : {}),
|
|
53
|
+
});
|
|
54
|
+
this.config.emitter.emit({ type: 'stage_complete', stageId: stageRun.id, stageName: map.map });
|
|
55
|
+
return { status, stageRun, output };
|
|
56
|
+
};
|
|
57
|
+
// Technical failure (bad `over`, missing spawner, depth limit) — record the
|
|
58
|
+
// reason on the stage as a failed task so it surfaces in `studio status`,
|
|
59
|
+
// the run JSONL, and the CLI's "Errors:" line, then fail the stage.
|
|
60
|
+
const failTechnical = (reason) => {
|
|
61
|
+
const now = new Date().toISOString();
|
|
62
|
+
const taskRun = {
|
|
63
|
+
id: randomUUID(),
|
|
64
|
+
task_name: map.map,
|
|
65
|
+
status: 'failed',
|
|
66
|
+
started_at: startedAt,
|
|
67
|
+
completed_at: now,
|
|
68
|
+
agent_runs: [{
|
|
69
|
+
id: randomUUID(),
|
|
70
|
+
agent_name: `map:${map.pipeline}`,
|
|
71
|
+
attempt: 1,
|
|
72
|
+
status: 'failed',
|
|
73
|
+
tool_calls: 0,
|
|
74
|
+
started_at: startedAt,
|
|
75
|
+
completed_at: now,
|
|
76
|
+
error: reason,
|
|
77
|
+
}],
|
|
78
|
+
};
|
|
79
|
+
stageRun.tasks = [taskRun];
|
|
80
|
+
return finish('failed');
|
|
81
|
+
};
|
|
82
|
+
this.config.events?.onStageStart?.({
|
|
83
|
+
stage_name: map.map,
|
|
84
|
+
stage_index: stageIndex,
|
|
85
|
+
total_stages: totalStages,
|
|
86
|
+
max_attempts: 1,
|
|
87
|
+
});
|
|
88
|
+
this.config.emitter.emit({ type: 'stage_start', stageId: stageRun.id, stageName: map.map });
|
|
89
|
+
// Condition — skip the whole fan-out if false
|
|
90
|
+
if (map.condition !== undefined) {
|
|
91
|
+
const shouldRun = evaluateCondition(map.condition, {
|
|
92
|
+
input: context.input,
|
|
93
|
+
stageOutputs: context.stageOutputs,
|
|
94
|
+
});
|
|
95
|
+
if (!shouldRun) {
|
|
96
|
+
return finish('skipped', undefined, `condition not met: ${map.condition}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (signal?.aborted)
|
|
100
|
+
return finish('cancelled');
|
|
101
|
+
// A map stage spawns child runs — it needs a spawner.
|
|
102
|
+
if (!this.config.spawner) {
|
|
103
|
+
return failTechnical(`Map stage '${map.map}' requires a run spawner, but none is configured. ` +
|
|
104
|
+
`Fan-out stages spawn sub-pipeline runs; construct the engine with a 'spawner'.`);
|
|
105
|
+
}
|
|
106
|
+
const spawner = this.config.spawner;
|
|
107
|
+
// Resolve the list to iterate.
|
|
108
|
+
const resolved = resolveContextPath(map.over, {
|
|
109
|
+
input: context.input,
|
|
110
|
+
stageOutputs: context.stageOutputs,
|
|
111
|
+
});
|
|
112
|
+
if (!Array.isArray(resolved)) {
|
|
113
|
+
return failTechnical(`Map stage '${map.map}': 'over: ${map.over}' did not resolve to an array ` +
|
|
114
|
+
`(got ${resolved === undefined ? 'undefined' : typeof resolved}). ` +
|
|
115
|
+
`It must reference a list, e.g. stages.<name>.output.items or input.items.`);
|
|
116
|
+
}
|
|
117
|
+
const items = resolved;
|
|
118
|
+
const concurrency = Math.max(1, map.concurrency ?? 1);
|
|
119
|
+
const failFast = (map.on_item_failure ?? 'fail-fast') === 'fail-fast';
|
|
120
|
+
this.config.events?.onMapStart?.({ map_name: map.map, total_items: items.length, concurrency });
|
|
121
|
+
this.config.emitter.emit({ type: 'map_start', mapName: map.map, totalItems: items.length });
|
|
122
|
+
if (items.length === 0) {
|
|
123
|
+
const emptyOutput = { total: 0, succeeded: 0, failed: 0, resumed: 0, results: [], outputs: [] };
|
|
124
|
+
this.config.events?.onMapComplete?.({ map_name: map.map, total: 0, succeeded: 0, failed: 0, status: 'success' });
|
|
125
|
+
this.config.emitter.emit({ type: 'map_complete', mapName: map.map, succeeded: 0, failed: 0, status: 'success' });
|
|
126
|
+
return finish('success', emptyOutput);
|
|
127
|
+
}
|
|
128
|
+
// Depth guard — mirror studio_run's recursion limit.
|
|
129
|
+
if (depth + 1 > this.config.maxDepth) {
|
|
130
|
+
return failTechnical(`Map stage '${map.map}' would spawn runs at depth ${depth + 1}, exceeding maxDepth ${this.config.maxDepth}. ` +
|
|
131
|
+
`Recursive fan-out is not allowed at this nesting level.`);
|
|
132
|
+
}
|
|
133
|
+
// Per-item resume — skip items a previous run already completed. Opt-in
|
|
134
|
+
// (`resume: true`) and a no-op without a configured cache. The key is the
|
|
135
|
+
// item *input*, so a reordered/filtered list still hits and a changed item
|
|
136
|
+
// misses (see MapStage.resume docs). Failures are never cached, so they
|
|
137
|
+
// retry next run; a cache-served item counts as a success and never trips
|
|
138
|
+
// fail-fast.
|
|
139
|
+
const resumeEnabled = map.resume === true && this.config.cache !== undefined;
|
|
140
|
+
const cache = this.config.cache;
|
|
141
|
+
const namespace = {
|
|
142
|
+
pipeline: pipelineName,
|
|
143
|
+
stage: map.map,
|
|
144
|
+
subPipeline: map.pipeline,
|
|
145
|
+
};
|
|
146
|
+
const results = new Array(items.length);
|
|
147
|
+
let cursor = 0;
|
|
148
|
+
let abortLaunch = false; // fail-fast: stop pulling new items after a failure
|
|
149
|
+
const worker = async () => {
|
|
150
|
+
for (;;) {
|
|
151
|
+
if (signal?.aborted || abortLaunch)
|
|
152
|
+
return;
|
|
153
|
+
const i = cursor++;
|
|
154
|
+
if (i >= items.length)
|
|
155
|
+
return;
|
|
156
|
+
const label = mapItemLabel(items[i], i);
|
|
157
|
+
// Resume check — before announcing the item as started, so a cache hit
|
|
158
|
+
// is served without a spinner. buildItemInput can throw for a bad item;
|
|
159
|
+
// fall through to the normal path, which surfaces that as an item failure.
|
|
160
|
+
let itemInput;
|
|
161
|
+
let cached;
|
|
162
|
+
if (resumeEnabled && cache) {
|
|
163
|
+
try {
|
|
164
|
+
itemInput = buildItemInput(map, items[i], i, context.input);
|
|
165
|
+
cached = await cache.get(namespace, hashItemInput(itemInput));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
itemInput = undefined;
|
|
169
|
+
cached = undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (cached) {
|
|
173
|
+
const itemResult = {
|
|
174
|
+
index: i,
|
|
175
|
+
status: 'success',
|
|
176
|
+
output: cached.output,
|
|
177
|
+
cached: true,
|
|
178
|
+
...(cached.run_id ? { run_id: cached.run_id } : {}),
|
|
179
|
+
};
|
|
180
|
+
results[i] = itemResult;
|
|
181
|
+
this.config.events?.onMapItemComplete?.({
|
|
182
|
+
map_name: map.map,
|
|
183
|
+
index: i,
|
|
184
|
+
total_items: items.length,
|
|
185
|
+
status: 'success',
|
|
186
|
+
label,
|
|
187
|
+
cached: true,
|
|
188
|
+
...(cached.run_id ? { run_id: cached.run_id } : {}),
|
|
189
|
+
});
|
|
190
|
+
this.config.emitter.emit({ type: 'map_item_complete', mapName: map.map, index: i, status: 'success' });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Name the item as it goes in flight, so a --live operator sees which
|
|
194
|
+
// items are running right now (not just an advancing count).
|
|
195
|
+
this.config.events?.onMapItemStart?.({
|
|
196
|
+
map_name: map.map,
|
|
197
|
+
index: i,
|
|
198
|
+
total_items: items.length,
|
|
199
|
+
label,
|
|
200
|
+
});
|
|
201
|
+
let itemResult;
|
|
202
|
+
try {
|
|
203
|
+
const input = itemInput ?? buildItemInput(map, items[i], i, context.input);
|
|
204
|
+
const spawn = await spawner.spawnAndWait({
|
|
205
|
+
pipeline: map.pipeline,
|
|
206
|
+
input,
|
|
207
|
+
parentRunId: runId,
|
|
208
|
+
depth: depth + 1,
|
|
209
|
+
});
|
|
210
|
+
itemResult = { index: i, status: 'success', output: spawn.output, run_id: spawn.run_id };
|
|
211
|
+
// Only successful items are cached — a failure stays un-cached so it
|
|
212
|
+
// retries next run.
|
|
213
|
+
if (resumeEnabled && cache) {
|
|
214
|
+
await cache.set(namespace, hashItemInput(input), {
|
|
215
|
+
output: spawn.output,
|
|
216
|
+
run_id: spawn.run_id,
|
|
217
|
+
cached_at: new Date().toISOString(),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
itemResult = { index: i, status: 'failed', error: errorMessage(err) };
|
|
223
|
+
if (failFast)
|
|
224
|
+
abortLaunch = true;
|
|
225
|
+
}
|
|
226
|
+
results[i] = itemResult;
|
|
227
|
+
this.config.events?.onMapItemComplete?.({
|
|
228
|
+
map_name: map.map,
|
|
229
|
+
index: i,
|
|
230
|
+
total_items: items.length,
|
|
231
|
+
status: itemResult.status,
|
|
232
|
+
label,
|
|
233
|
+
...(itemResult.run_id ? { run_id: itemResult.run_id } : {}),
|
|
234
|
+
...(itemResult.error ? { error: itemResult.error } : {}),
|
|
235
|
+
});
|
|
236
|
+
this.config.emitter.emit({ type: 'map_item_complete', mapName: map.map, index: i, status: itemResult.status });
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
240
|
+
// Cancelled mid-flight → cancelled (don't report success/failure of a partial batch).
|
|
241
|
+
if (signal?.aborted)
|
|
242
|
+
return finish('cancelled');
|
|
243
|
+
const settled = results.filter((r) => r !== undefined);
|
|
244
|
+
settled.sort((a, b) => a.index - b.index);
|
|
245
|
+
const succeeded = settled.filter(r => r.status === 'success');
|
|
246
|
+
const failed = settled.filter(r => r.status === 'failed');
|
|
247
|
+
const output = {
|
|
248
|
+
total: items.length,
|
|
249
|
+
succeeded: succeeded.length,
|
|
250
|
+
failed: failed.length,
|
|
251
|
+
resumed: succeeded.filter(r => r.cached).length,
|
|
252
|
+
results: settled,
|
|
253
|
+
outputs: succeeded.map(r => r.output),
|
|
254
|
+
};
|
|
255
|
+
// Status policy:
|
|
256
|
+
// fail-fast → any failure fails the stage.
|
|
257
|
+
// collect-all → succeeds as long as at least one item succeeded; a batch
|
|
258
|
+
// where every item failed is a real failure, not per-item noise.
|
|
259
|
+
let status;
|
|
260
|
+
if (failFast) {
|
|
261
|
+
status = failed.length > 0 ? 'failed' : 'success';
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
status = succeeded.length > 0 ? 'success' : 'failed';
|
|
265
|
+
}
|
|
266
|
+
this.config.events?.onMapComplete?.({
|
|
267
|
+
map_name: map.map,
|
|
268
|
+
total: items.length,
|
|
269
|
+
succeeded: succeeded.length,
|
|
270
|
+
failed: failed.length,
|
|
271
|
+
status,
|
|
272
|
+
});
|
|
273
|
+
this.config.emitter.emit({
|
|
274
|
+
type: 'map_complete',
|
|
275
|
+
mapName: map.map,
|
|
276
|
+
succeeded: succeeded.length,
|
|
277
|
+
failed: failed.length,
|
|
278
|
+
status,
|
|
279
|
+
});
|
|
280
|
+
return finish(status, output);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=map-orchestrator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-orchestrator.js","sourceRoot":"","sources":["../../src/pipeline/map-orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-checker.d.ts","sourceRoot":"","sources":["../../src/pipeline/output-checker.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEtF,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,gBAAgB,CAAC,CAa3B"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Post-execution filesystem check for a stage's declared outputs.
|
|
2
|
+
//
|
|
3
|
+
// A stage returning "success" only proves the agent finished — not that it
|
|
4
|
+
// actually wrote the files it was supposed to. This is the orchestrator
|
|
5
|
+
// responsibility that was previously bolted onto callers (e.g. run_wiki.py's
|
|
6
|
+
// `check_outputs`/`required_files`: "succeeded but expected files are missing").
|
|
7
|
+
// Declaring `expected_outputs.files` in the contract moves that check into the
|
|
8
|
+
// engine, where it belongs.
|
|
9
|
+
//
|
|
10
|
+
// Each entry is a path or glob relative to the repo workspace and must match at
|
|
11
|
+
// least one existing file. A miss is a validation error, so it flows into the
|
|
12
|
+
// RALPH loop like any other validator: the agent gets a retry with enriched
|
|
13
|
+
// feedback, and the stage fails only once attempts are exhausted.
|
|
14
|
+
import { glob } from 'node:fs/promises';
|
|
15
|
+
export async function checkExpectedOutputs(expected, cwd) {
|
|
16
|
+
if (!expected?.files || expected.files.length === 0) {
|
|
17
|
+
return { valid: true, errors: [], warnings: [] };
|
|
18
|
+
}
|
|
19
|
+
const errors = [];
|
|
20
|
+
for (const pattern of expected.files) {
|
|
21
|
+
if (!(await matchesAny(pattern, cwd))) {
|
|
22
|
+
errors.push(`Expected output missing: no file matches '${pattern}'`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { valid: errors.length === 0, errors, warnings: [] };
|
|
26
|
+
}
|
|
27
|
+
/** True as soon as one path matches the pattern under cwd. Fails closed on error. */
|
|
28
|
+
async function matchesAny(pattern, cwd) {
|
|
29
|
+
try {
|
|
30
|
+
for await (const _match of glob(pattern, { cwd })) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// An unreadable directory or bad pattern → treat as "not found", not a crash.
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=output-checker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-checker.js","sourceRoot":"","sources":["../../src/pipeline/output-checker.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2EAA2E;AAC3E,wEAAwE;AACxE,6EAA6E;AAC7E,iFAAiF;AACjF,+EAA+E;AAC/E,4BAA4B;AAC5B,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,4EAA4E;AAC5E,kEAAkE;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAGxC,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,QAAqC,EACrC,GAAW;IAEX,IAAI,CAAC,QAAQ,EAAE,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IACnD,CAAC;IAED,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QACrC,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC;YACtC,MAAM,CAAC,IAAI,CAAC,6CAA6C,OAAO,GAAG,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAC9D,CAAC;AAED,qFAAqF;AACrF,KAAK,UAAU,UAAU,CAAC,OAAe,EAAE,GAAW;IACpD,IAAI,CAAC;QACH,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;YAClD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,8EAA8E;QAC9E,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-checker.test.d.ts","sourceRoot":"","sources":["../../src/pipeline/output-checker.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { checkExpectedOutputs } from './output-checker.js';
|
|
6
|
+
describe('checkExpectedOutputs', () => {
|
|
7
|
+
let dir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
dir = await mkdtemp(join(tmpdir(), 'output-checker-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await rm(dir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('is valid when there are no expected outputs', async () => {
|
|
15
|
+
expect(await checkExpectedOutputs(undefined, dir)).toEqual({
|
|
16
|
+
valid: true,
|
|
17
|
+
errors: [],
|
|
18
|
+
warnings: [],
|
|
19
|
+
});
|
|
20
|
+
expect((await checkExpectedOutputs({ files: [] }, dir)).valid).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('passes when a declared literal file exists', async () => {
|
|
23
|
+
await writeFile(join(dir, 'wiki_pages.json'), '{}');
|
|
24
|
+
const res = await checkExpectedOutputs({ files: ['wiki_pages.json'] }, dir);
|
|
25
|
+
expect(res.valid).toBe(true);
|
|
26
|
+
expect(res.errors).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
it('fails when a declared literal file is missing', async () => {
|
|
29
|
+
const res = await checkExpectedOutputs({ files: ['wiki_pages.json'] }, dir);
|
|
30
|
+
expect(res.valid).toBe(false);
|
|
31
|
+
expect(res.errors).toEqual([
|
|
32
|
+
"Expected output missing: no file matches 'wiki_pages.json'",
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
it('passes when a glob pattern matches at least one file', async () => {
|
|
36
|
+
await writeFile(join(dir, 'batch_1.json'), '{}');
|
|
37
|
+
await writeFile(join(dir, 'batch_2.json'), '{}');
|
|
38
|
+
const res = await checkExpectedOutputs({ files: ['batch_*.json'] }, dir);
|
|
39
|
+
expect(res.valid).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it('fails when a glob pattern matches nothing', async () => {
|
|
42
|
+
const res = await checkExpectedOutputs({ files: ['batch_*.json'] }, dir);
|
|
43
|
+
expect(res.valid).toBe(false);
|
|
44
|
+
expect(res.errors[0]).toContain('batch_*.json');
|
|
45
|
+
});
|
|
46
|
+
it('matches files in nested directories', async () => {
|
|
47
|
+
await mkdir(join(dir, 'out'), { recursive: true });
|
|
48
|
+
await writeFile(join(dir, 'out', 'result.md'), '# hi');
|
|
49
|
+
expect((await checkExpectedOutputs({ files: ['out/result.md'] }, dir)).valid).toBe(true);
|
|
50
|
+
expect((await checkExpectedOutputs({ files: ['**/*.md'] }, dir)).valid).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it('reports one error per missing entry and passes the present ones', async () => {
|
|
53
|
+
await writeFile(join(dir, 'present.json'), '{}');
|
|
54
|
+
const res = await checkExpectedOutputs({ files: ['present.json', 'missing_a.json', 'missing_b.json'] }, dir);
|
|
55
|
+
expect(res.valid).toBe(false);
|
|
56
|
+
expect(res.errors).toHaveLength(2);
|
|
57
|
+
expect(res.errors.some((e) => e.includes('missing_a.json'))).toBe(true);
|
|
58
|
+
expect(res.errors.some((e) => e.includes('missing_b.json'))).toBe(true);
|
|
59
|
+
expect(res.errors.some((e) => e.includes('present.json'))).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=output-checker.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-checker.test.js","sourceRoot":"","sources":["../../src/pipeline/output-checker.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage-executor.d.ts","sourceRoot":"","sources":["../../src/pipeline/stage-executor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,eAAe,EAMhB,MAAM,8BAA8B,CAAC;AAgBtC,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAErB,uBAAuB,EACxB,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"stage-executor.d.ts","sourceRoot":"","sources":["../../src/pipeline/stage-executor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,eAAe,EAMhB,MAAM,8BAA8B,CAAC;AAgBtC,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAErB,uBAAuB,EACxB,MAAM,2BAA2B,CAAC;AAMnC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAQlC,OAAO,KAAK,EAAE,YAAY,EAAqB,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAY5D,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,mBAAmB;IAEzC,OAAO,CACX,QAAQ,EAAE,eAAe,EACzB,eAAe,EAAE,eAAe,EAChC,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,YAAY,EACnB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,aAAa,CAAC,EAAE,uBAAuB,GAAG,IAAI,EAC9C,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC;IAofvB,OAAO,CAAC,cAAc;IAqEtB,OAAO,CAAC,oBAAoB;YAgBd,aAAa;CAa5B"}
|
|
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { ralph, validateSchema, validateToolCalls, validateRequiredTools, validateCountedTools, validateToolGroups, compose, exponentialBackoff, fixedDelay, noDelay, } from '@studio-foundation/ralph';
|
|
6
6
|
import { runAgent, runScript, AnonymizationMiddleware, } from '@studio-foundation/runner';
|
|
7
|
+
import { buildTaskInput } from './task-input.js';
|
|
7
8
|
import { loadAgentProfile } from './agent-loader.js';
|
|
8
9
|
import { loadContract } from './contract-loader.js';
|
|
9
10
|
import { loadSkillFiles } from './skill-loader.js';
|
|
@@ -13,6 +14,8 @@ import { evaluateCondition } from './condition-evaluator.js';
|
|
|
13
14
|
import { deriveStageStatus } from '../state/status-derivation.js';
|
|
14
15
|
import { transition } from '../state/state-machine.js';
|
|
15
16
|
import { postValidate } from './post-validator.js';
|
|
17
|
+
import { runExternalValidators } from './external-validator.js';
|
|
18
|
+
import { checkExpectedOutputs } from './output-checker.js';
|
|
16
19
|
import { loadContextPacks } from './context-pack-loader.js';
|
|
17
20
|
// Module-level helpers — verbatim from engine.ts lines 73-95
|
|
18
21
|
function summarizeOutput(output) {
|
|
@@ -139,7 +142,7 @@ export class StageExecutor {
|
|
|
139
142
|
};
|
|
140
143
|
this.config.events.onStageContext(contextEvent);
|
|
141
144
|
}
|
|
142
|
-
// Create a single task run (
|
|
145
|
+
// Create a single task run (1 stage = 1 task = 1 ralph call)
|
|
143
146
|
const taskRun = {
|
|
144
147
|
id: randomUUID(),
|
|
145
148
|
task_name: stageDef.name,
|
|
@@ -246,10 +249,7 @@ export class StageExecutor {
|
|
|
246
249
|
executor: async (execContext) => {
|
|
247
250
|
const agentRunId = randomUUID();
|
|
248
251
|
const agentRunStartedAt = new Date().toISOString();
|
|
249
|
-
const taskInput =
|
|
250
|
-
description: typeof userInput === 'string' ? userInput : JSON.stringify(userInput),
|
|
251
|
-
contract_name: contract?.name,
|
|
252
|
-
};
|
|
252
|
+
const taskInput = buildTaskInput(userInput, contract?.name);
|
|
253
253
|
// Map ralph context to runner context
|
|
254
254
|
const runnerExecContext = {
|
|
255
255
|
attempt: execContext.attempt,
|
|
@@ -299,16 +299,21 @@ export class StageExecutor {
|
|
|
299
299
|
cwd: this.config.repoPath ?? this.config.configsDir,
|
|
300
300
|
timeoutMs: stageDef.timeout_ms,
|
|
301
301
|
});
|
|
302
|
-
// Record agent run
|
|
302
|
+
// Record agent run. A terminal executor error (a crashed script, max
|
|
303
|
+
// tool iterations) must land on the run as status 'failed' + error —
|
|
304
|
+
// otherwise the reason the runner already captured (e.g. a python
|
|
305
|
+
// stage's stderr) never reaches `studio status`, the run JSONL, or the
|
|
306
|
+
// CLI's "Errors:" line, which read the last agent run's error. (STU-568)
|
|
303
307
|
const agentRun = {
|
|
304
308
|
id: agentRunId,
|
|
305
309
|
agent_name: agentConfig?.name ?? `script:${stageDef.script ?? 'unknown'}`,
|
|
306
310
|
attempt: execContext.attempt,
|
|
307
|
-
status: 'success',
|
|
311
|
+
status: result.error ? 'failed' : 'success',
|
|
308
312
|
tool_calls: result.tool_calls_count,
|
|
309
313
|
started_at: agentRunStartedAt,
|
|
310
314
|
completed_at: new Date().toISOString(),
|
|
311
315
|
output: result.output,
|
|
316
|
+
...(result.error ? { error: result.error } : {}),
|
|
312
317
|
};
|
|
313
318
|
taskRun.agent_runs.push(agentRun);
|
|
314
319
|
return result;
|
|
@@ -316,6 +321,12 @@ export class StageExecutor {
|
|
|
316
321
|
validator: ralphValidator,
|
|
317
322
|
maxAttempts: stageDef.ralph?.max_attempts ?? 3,
|
|
318
323
|
retryStrategy,
|
|
324
|
+
// A script stage is deterministic: re-running the same command on the
|
|
325
|
+
// same stdin yields the same result, so a process-level error (a startup
|
|
326
|
+
// crash, a non-zero exit, a timeout) will not heal on retry. Fail fast
|
|
327
|
+
// instead of burning every attempt on an identical crash. Agent stages
|
|
328
|
+
// are stochastic and keep their retries. (STU-568)
|
|
329
|
+
isFatal: stageDef.agent ? undefined : (result) => result.error != null,
|
|
319
330
|
signal,
|
|
320
331
|
onRetry: async (event) => {
|
|
321
332
|
// Extract raw output for diagnostic logging
|
|
@@ -343,6 +354,21 @@ export class StageExecutor {
|
|
|
343
354
|
catch (err) {
|
|
344
355
|
// AbortError from signal propagation is handled inside ralph (returns 'cancelled').
|
|
345
356
|
// Any other throw is a technical failure (network, timeout, etc.) — mark stage failed.
|
|
357
|
+
// Record it as a failed agent run: the executor throws before it can push one, so
|
|
358
|
+
// without this every reader (`studio status`, `logs`, the run JSONL, the CLI's own
|
|
359
|
+
// "Errors:" line) reports a stage that failed for no stated reason (STU-561).
|
|
360
|
+
taskRun.agent_runs.push({
|
|
361
|
+
id: randomUUID(),
|
|
362
|
+
agent_name: agentConfig?.name ?? `script:${stageDef.script ?? 'unknown'}`,
|
|
363
|
+
attempt: 1,
|
|
364
|
+
status: 'failed',
|
|
365
|
+
tool_calls: 0,
|
|
366
|
+
started_at: stageStartedAt,
|
|
367
|
+
completed_at: new Date().toISOString(),
|
|
368
|
+
error: err instanceof Error ? err.message : String(err),
|
|
369
|
+
});
|
|
370
|
+
taskRun.status = 'failed';
|
|
371
|
+
taskRun.completed_at = new Date().toISOString();
|
|
346
372
|
stageRun.status = transition('running', 'fail');
|
|
347
373
|
stageRun.completed_at = new Date().toISOString();
|
|
348
374
|
stageRun.tasks = [taskRun];
|
|
@@ -478,8 +504,8 @@ export class StageExecutor {
|
|
|
478
504
|
}
|
|
479
505
|
// Compose ralph's built-in validators
|
|
480
506
|
const validators = [errorCheck];
|
|
481
|
-
// Schema validation (required_fields)
|
|
482
|
-
if (contract.schema?.required_fields) {
|
|
507
|
+
// Schema validation (required_fields presence + field-level type/enum/nested)
|
|
508
|
+
if (contract.schema?.required_fields || contract.schema?.fields) {
|
|
483
509
|
validators.push((result) => validateSchema(result.output, contract));
|
|
484
510
|
}
|
|
485
511
|
// Normalize tool call requirements from contract or stage definition
|
|
@@ -501,6 +527,19 @@ export class StageExecutor {
|
|
|
501
527
|
if (toolCallReqs?.required_tool_groups?.length) {
|
|
502
528
|
validators.push((result) => validateToolGroups(result.tool_calls, toolCallReqs));
|
|
503
529
|
}
|
|
530
|
+
// External validators — run a command against the REAL output (not tool args).
|
|
531
|
+
// Enforces what the declarative schema can't (cross-field rules, computed
|
|
532
|
+
// constraints); field-level types/enums/nesting live in schema.fields.
|
|
533
|
+
if (contract.validators?.length) {
|
|
534
|
+
const cwd = this.config.repoPath ?? this.config.configsDir;
|
|
535
|
+
validators.push((result) => runExternalValidators(result.output, contract.validators, cwd));
|
|
536
|
+
}
|
|
537
|
+
// Expected outputs — check the filesystem for the files the stage must leave
|
|
538
|
+
// on disk. A "success" return code doesn't prove the artifacts exist; this does.
|
|
539
|
+
if (contract.expected_outputs?.files?.length) {
|
|
540
|
+
const cwd = this.config.repoPath ?? this.config.configsDir;
|
|
541
|
+
validators.push(() => checkExpectedOutputs(contract.expected_outputs, cwd));
|
|
542
|
+
}
|
|
504
543
|
if (validators.length === 0) {
|
|
505
544
|
return () => ({ valid: true, errors: [], warnings: [] });
|
|
506
545
|
}
|