@ryanfw/prompt-orchestration-pipeline 1.3.1 → 1.3.2
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/docs/pop-task-guide.md +1 -0
- package/package.json +1 -2
- package/src/core/__tests__/agent-step.test.ts +322 -3
- package/src/core/__tests__/task-runner.test.ts +57 -1
- package/src/core/agent-step.ts +231 -14
- package/src/core/agent-types.ts +3 -0
- package/src/core/file-io.ts +8 -74
- package/src/core/pipeline-runner.ts +54 -3
- package/src/core/status-writer.ts +44 -26
- package/src/core/task-runner.ts +27 -3
- package/src/harness/__tests__/discovery.test.ts +60 -8
- package/src/harness/discovery.ts +16 -6
- package/src/ui/client/hooks/useJobListWithUpdates.ts +16 -1
- package/src/ui/dist/assets/{index-CbS3OsW7.js → index--RH3sAt3.js} +14 -1
- package/src/ui/dist/assets/{index-CbS3OsW7.js.map → index--RH3sAt3.js.map} +1 -1
- package/src/ui/dist/assets/style-CSSKuMOe.css +2 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/embedded-assets.js +6 -6
- package/src/ui/server/__tests__/gate-endpoints.test.ts +90 -0
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +188 -0
- package/src/ui/server/__tests__/path-containment.test.ts +54 -0
- package/src/ui/server/__tests__/status-corruption.test.ts +55 -0
- package/src/ui/server/config-bridge.ts +1 -0
- package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +77 -0
- package/src/ui/server/endpoints/gate-endpoints.ts +17 -1
- package/src/ui/server/endpoints/job-control-endpoints.ts +36 -2
- package/src/ui/server/endpoints/upload-endpoints.ts +13 -3
- package/src/ui/server/utils/http-utils.ts +6 -1
- package/src/ui/server/utils/path-containment.ts +18 -0
- package/src/ui/server/utils/status-corruption.ts +27 -0
- package/src/ui/dist/assets/style-BUFg3Sth.css +0 -2
package/docs/pop-task-guide.md
CHANGED
|
@@ -207,6 +207,7 @@ export const inference = async ({ runAgent, io, data, flags }) => {
|
|
|
207
207
|
// model?: string // optional, passed through to the CLI
|
|
208
208
|
// io?: boolean // default true: bridge POP read/write artifacts
|
|
209
209
|
// timeoutMs?: number // optional wall-clock cap
|
|
210
|
+
// idleTimeoutMs?: number // optional no-output cap
|
|
210
211
|
// captureDiff?: boolean // capture a git diff as 'agent.patch'
|
|
211
212
|
});
|
|
212
213
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryanfw/prompt-orchestration-pipeline",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "A Prompt-orchestration pipeline (POP) is a framework for building, running, and experimenting with complex chains of LLM tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/ui/server/index.ts",
|
|
@@ -64,7 +64,6 @@
|
|
|
64
64
|
"react": "^19.2.0",
|
|
65
65
|
"react-dom": "^19.2.0",
|
|
66
66
|
"react-markdown": "^10.1.0",
|
|
67
|
-
"react-mentions": "^4.4.10",
|
|
68
67
|
"react-router-dom": "^7.9.4",
|
|
69
68
|
"react-syntax-highlighter": "^15.6.1",
|
|
70
69
|
"rehype-highlight": "^7.0.2",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import type { WriteOptions, TaskFileIO } from "../file-io.ts";
|
|
@@ -53,10 +53,11 @@ function createFakeIO(): TaskFileIO & { calls: string[] } {
|
|
|
53
53
|
|
|
54
54
|
function createFakeMcpHandle(
|
|
55
55
|
artifacts: string[] = [],
|
|
56
|
+
connection = { url: "http://127.0.0.1:9999/mcp", token: "fake-token" },
|
|
56
57
|
): McpIoServerHandle & { closeSpy: ReturnType<typeof mock> } {
|
|
57
58
|
const closeSpy = mock(async () => {});
|
|
58
59
|
return {
|
|
59
|
-
connection
|
|
60
|
+
connection,
|
|
60
61
|
artifactsWritten() {
|
|
61
62
|
return [...artifacts];
|
|
62
63
|
},
|
|
@@ -106,6 +107,26 @@ function makeSuccessResult(overrides?: Partial<RunResult>): RunResult {
|
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void; reject: (err: unknown) => void } {
|
|
111
|
+
let resolve!: (value: T) => void;
|
|
112
|
+
let reject!: (err: unknown) => void;
|
|
113
|
+
const promise = new Promise<T>((res, rej) => {
|
|
114
|
+
resolve = res;
|
|
115
|
+
reject = rej;
|
|
116
|
+
});
|
|
117
|
+
return { promise, resolve, reject };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function waitFor(predicate: () => boolean, message: string): Promise<void> {
|
|
121
|
+
const deadline = Date.now() + 1_000;
|
|
122
|
+
while (!predicate()) {
|
|
123
|
+
if (Date.now() > deadline) {
|
|
124
|
+
throw new Error(message);
|
|
125
|
+
}
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
109
130
|
function makeDeps(result: RunResult | Error) {
|
|
110
131
|
const run = mock(() => makeFakeHarnessRun(result));
|
|
111
132
|
const startMcpIoServer = mock(async () => createFakeMcpHandle());
|
|
@@ -178,6 +199,281 @@ describe("executeAgent", () => {
|
|
|
178
199
|
|
|
179
200
|
expect(startMcpIoServer).not.toHaveBeenCalled();
|
|
180
201
|
});
|
|
202
|
+
|
|
203
|
+
it("passes adapter-native MCP servers for claude", async () => {
|
|
204
|
+
const io = createFakeIO();
|
|
205
|
+
let capturedMcpServers: unknown;
|
|
206
|
+
const run = mock((opts: { mcpServers?: unknown }) => {
|
|
207
|
+
capturedMcpServers = opts.mcpServers;
|
|
208
|
+
return makeFakeHarnessRun(makeSuccessResult());
|
|
209
|
+
});
|
|
210
|
+
const startMcpIoServer = mock(async () => createFakeMcpHandle());
|
|
211
|
+
|
|
212
|
+
await executeAgent(
|
|
213
|
+
{ io, entry: { name: "a", harness: "claude", prompt: "p" } },
|
|
214
|
+
{ run, startMcpIoServer },
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(capturedMcpServers).toEqual([{
|
|
218
|
+
name: "popio",
|
|
219
|
+
url: "http://127.0.0.1:9999/mcp",
|
|
220
|
+
headers: { Authorization: "Bearer fake-token" },
|
|
221
|
+
}]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("bridges MCP for codex through a temporary binary wrapper", async () => {
|
|
225
|
+
const io = createFakeIO();
|
|
226
|
+
const previousPopBin = process.env.POP_CODEX_BIN;
|
|
227
|
+
const previousAdapterBin = process.env.LLM_ADAPTER_CODEX_BIN;
|
|
228
|
+
const previousRealBin = process.env.POP_REAL_CODEX_BIN;
|
|
229
|
+
const previousMcpUrlConfig = process.env.POP_MCP_URL_CONFIG;
|
|
230
|
+
const previousMcpToken = process.env.POP_MCP_TOKEN;
|
|
231
|
+
let wrapperPath = "";
|
|
232
|
+
let capturedMcpServers: unknown;
|
|
233
|
+
try {
|
|
234
|
+
process.env.POP_CODEX_BIN = "/custom/codex";
|
|
235
|
+
delete process.env.LLM_ADAPTER_CODEX_BIN;
|
|
236
|
+
delete process.env.POP_REAL_CODEX_BIN;
|
|
237
|
+
delete process.env.POP_MCP_URL_CONFIG;
|
|
238
|
+
delete process.env.POP_MCP_TOKEN;
|
|
239
|
+
const run = mock((opts: { mcpServers?: unknown; onEvent?: (event: HarnessEvent) => void }) => {
|
|
240
|
+
capturedMcpServers = opts.mcpServers;
|
|
241
|
+
wrapperPath = process.env.LLM_ADAPTER_CODEX_BIN ?? "";
|
|
242
|
+
expect(wrapperPath).toContain("codex-mcp-");
|
|
243
|
+
expect(readFileSync(wrapperPath, "utf8")).toContain("POP_MCP_URL_CONFIG");
|
|
244
|
+
expect(process.env.POP_REAL_CODEX_BIN).toBe("/custom/codex");
|
|
245
|
+
expect(process.env.POP_MCP_URL_CONFIG).toBe(
|
|
246
|
+
'mcp_servers.popio.url="http://127.0.0.1:9999/mcp"',
|
|
247
|
+
);
|
|
248
|
+
expect(process.env.POP_MCP_TOKEN).toBe("fake-token");
|
|
249
|
+
opts.onEvent?.({
|
|
250
|
+
harness: "codex",
|
|
251
|
+
seq: 0,
|
|
252
|
+
at: Date.now(),
|
|
253
|
+
raw: { type: "result", total_cost_usd: 0.42 },
|
|
254
|
+
type: "raw",
|
|
255
|
+
});
|
|
256
|
+
return makeFakeHarnessRun(makeSuccessResult());
|
|
257
|
+
});
|
|
258
|
+
const startMcpIoServer = mock(async () => createFakeMcpHandle());
|
|
259
|
+
|
|
260
|
+
const result = await executeAgent(
|
|
261
|
+
{ io, entry: { name: "a", harness: "codex", prompt: "p" } },
|
|
262
|
+
{ run, startMcpIoServer },
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(capturedMcpServers).toBeUndefined();
|
|
266
|
+
expect(result.costUsd).toBe(0.42);
|
|
267
|
+
expect(process.env.LLM_ADAPTER_CODEX_BIN).toBeUndefined();
|
|
268
|
+
expect(process.env.POP_REAL_CODEX_BIN).toBeUndefined();
|
|
269
|
+
expect(process.env.POP_MCP_URL_CONFIG).toBeUndefined();
|
|
270
|
+
expect(process.env.POP_MCP_TOKEN).toBeUndefined();
|
|
271
|
+
expect(existsSync(wrapperPath)).toBe(false);
|
|
272
|
+
} finally {
|
|
273
|
+
if (previousPopBin === undefined) {
|
|
274
|
+
delete process.env.POP_CODEX_BIN;
|
|
275
|
+
} else {
|
|
276
|
+
process.env.POP_CODEX_BIN = previousPopBin;
|
|
277
|
+
}
|
|
278
|
+
if (previousAdapterBin === undefined) {
|
|
279
|
+
delete process.env.LLM_ADAPTER_CODEX_BIN;
|
|
280
|
+
} else {
|
|
281
|
+
process.env.LLM_ADAPTER_CODEX_BIN = previousAdapterBin;
|
|
282
|
+
}
|
|
283
|
+
if (previousRealBin === undefined) {
|
|
284
|
+
delete process.env.POP_REAL_CODEX_BIN;
|
|
285
|
+
} else {
|
|
286
|
+
process.env.POP_REAL_CODEX_BIN = previousRealBin;
|
|
287
|
+
}
|
|
288
|
+
if (previousMcpUrlConfig === undefined) {
|
|
289
|
+
delete process.env.POP_MCP_URL_CONFIG;
|
|
290
|
+
} else {
|
|
291
|
+
process.env.POP_MCP_URL_CONFIG = previousMcpUrlConfig;
|
|
292
|
+
}
|
|
293
|
+
if (previousMcpToken === undefined) {
|
|
294
|
+
delete process.env.POP_MCP_TOKEN;
|
|
295
|
+
} else {
|
|
296
|
+
process.env.POP_MCP_TOKEN = previousMcpToken;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("serializes env-backed MCP compatibility for concurrent codex runs", async () => {
|
|
302
|
+
const previousPopBin = process.env.POP_CODEX_BIN;
|
|
303
|
+
const previousAdapterBin = process.env.LLM_ADAPTER_CODEX_BIN;
|
|
304
|
+
const previousRealBin = process.env.POP_REAL_CODEX_BIN;
|
|
305
|
+
const previousMcpUrlConfig = process.env.POP_MCP_URL_CONFIG;
|
|
306
|
+
const previousMcpToken = process.env.POP_MCP_TOKEN;
|
|
307
|
+
const firstRunResult = deferred<RunResult>();
|
|
308
|
+
const secondRunResult = deferred<RunResult>();
|
|
309
|
+
const observedTokens: Array<string | undefined> = [];
|
|
310
|
+
let runCount = 0;
|
|
311
|
+
let startCount = 0;
|
|
312
|
+
try {
|
|
313
|
+
process.env.POP_CODEX_BIN = "/custom/codex";
|
|
314
|
+
delete process.env.LLM_ADAPTER_CODEX_BIN;
|
|
315
|
+
delete process.env.POP_REAL_CODEX_BIN;
|
|
316
|
+
delete process.env.POP_MCP_URL_CONFIG;
|
|
317
|
+
delete process.env.POP_MCP_TOKEN;
|
|
318
|
+
const run = mock(() => {
|
|
319
|
+
runCount += 1;
|
|
320
|
+
observedTokens.push(process.env.POP_MCP_TOKEN);
|
|
321
|
+
return {
|
|
322
|
+
result: runCount === 1 ? firstRunResult.promise : secondRunResult.promise,
|
|
323
|
+
sessionId: Promise.resolve(`sess-${runCount}`),
|
|
324
|
+
abort() {},
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
const startMcpIoServer = mock(async () => {
|
|
328
|
+
startCount += 1;
|
|
329
|
+
return createFakeMcpHandle([], {
|
|
330
|
+
url: `http://127.0.0.1:999${startCount}/mcp`,
|
|
331
|
+
token: `token-${startCount}`,
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const first = executeAgent(
|
|
336
|
+
{ io: createFakeIO(), entry: { name: "a", harness: "codex", prompt: "p" } },
|
|
337
|
+
{ run, startMcpIoServer },
|
|
338
|
+
);
|
|
339
|
+
const second = executeAgent(
|
|
340
|
+
{ io: createFakeIO(), entry: { name: "b", harness: "codex", prompt: "p" } },
|
|
341
|
+
{ run, startMcpIoServer },
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
await waitFor(() => runCount === 1, "first codex run did not start");
|
|
345
|
+
expect(runCount).toBe(1);
|
|
346
|
+
expect(observedTokens).toHaveLength(1);
|
|
347
|
+
const firstObservedToken = observedTokens[0];
|
|
348
|
+
if (firstObservedToken === undefined) {
|
|
349
|
+
throw new Error("first codex run did not receive an MCP token");
|
|
350
|
+
}
|
|
351
|
+
expect(["token-1", "token-2"]).toContain(firstObservedToken);
|
|
352
|
+
|
|
353
|
+
firstRunResult.resolve(makeSuccessResult({ finalMessage: "run-1" }));
|
|
354
|
+
await waitFor(() => runCount === 2, "second codex run did not start");
|
|
355
|
+
expect(runCount).toBe(2);
|
|
356
|
+
expect(new Set(observedTokens)).toEqual(new Set(["token-1", "token-2"]));
|
|
357
|
+
|
|
358
|
+
secondRunResult.resolve(makeSuccessResult({ finalMessage: "run-2" }));
|
|
359
|
+
const results = await Promise.all([first, second]);
|
|
360
|
+
expect(new Set(results.map((r) => r.finalMessage))).toEqual(new Set(["run-1", "run-2"]));
|
|
361
|
+
expect(process.env.POP_MCP_TOKEN).toBeUndefined();
|
|
362
|
+
expect(process.env.POP_MCP_URL_CONFIG).toBeUndefined();
|
|
363
|
+
} finally {
|
|
364
|
+
firstRunResult.resolve(makeSuccessResult());
|
|
365
|
+
secondRunResult.resolve(makeSuccessResult());
|
|
366
|
+
if (previousPopBin === undefined) {
|
|
367
|
+
delete process.env.POP_CODEX_BIN;
|
|
368
|
+
} else {
|
|
369
|
+
process.env.POP_CODEX_BIN = previousPopBin;
|
|
370
|
+
}
|
|
371
|
+
if (previousAdapterBin === undefined) {
|
|
372
|
+
delete process.env.LLM_ADAPTER_CODEX_BIN;
|
|
373
|
+
} else {
|
|
374
|
+
process.env.LLM_ADAPTER_CODEX_BIN = previousAdapterBin;
|
|
375
|
+
}
|
|
376
|
+
if (previousRealBin === undefined) {
|
|
377
|
+
delete process.env.POP_REAL_CODEX_BIN;
|
|
378
|
+
} else {
|
|
379
|
+
process.env.POP_REAL_CODEX_BIN = previousRealBin;
|
|
380
|
+
}
|
|
381
|
+
if (previousMcpUrlConfig === undefined) {
|
|
382
|
+
delete process.env.POP_MCP_URL_CONFIG;
|
|
383
|
+
} else {
|
|
384
|
+
process.env.POP_MCP_URL_CONFIG = previousMcpUrlConfig;
|
|
385
|
+
}
|
|
386
|
+
if (previousMcpToken === undefined) {
|
|
387
|
+
delete process.env.POP_MCP_TOKEN;
|
|
388
|
+
} else {
|
|
389
|
+
process.env.POP_MCP_TOKEN = previousMcpToken;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("bridges MCP for opencode through OPENCODE_CONFIG_DIR and extracts raw cost", async () => {
|
|
395
|
+
const io = createFakeIO();
|
|
396
|
+
const previousConfigDir = process.env.OPENCODE_CONFIG_DIR;
|
|
397
|
+
const sourceConfigDir = mkdtempSync(join(tmpdir(), "opencode-existing-"));
|
|
398
|
+
let configDir = "";
|
|
399
|
+
let capturedMcpServers: unknown;
|
|
400
|
+
try {
|
|
401
|
+
writeFileSync(
|
|
402
|
+
join(sourceConfigDir, "opencode.json"),
|
|
403
|
+
JSON.stringify({
|
|
404
|
+
provider: "kept",
|
|
405
|
+
mcp: {
|
|
406
|
+
existing: { type: "local", command: ["tool"] },
|
|
407
|
+
},
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
410
|
+
process.env.OPENCODE_CONFIG_DIR = sourceConfigDir;
|
|
411
|
+
const run = mock((opts: { mcpServers?: unknown; onEvent?: (event: HarnessEvent) => void }) => {
|
|
412
|
+
capturedMcpServers = opts.mcpServers;
|
|
413
|
+
configDir = process.env.OPENCODE_CONFIG_DIR ?? "";
|
|
414
|
+
const config = JSON.parse(readFileSync(join(configDir, "opencode.json"), "utf8")) as Record<string, unknown>;
|
|
415
|
+
expect(config).toMatchObject({
|
|
416
|
+
provider: "kept",
|
|
417
|
+
mcp: {
|
|
418
|
+
existing: { type: "local", command: ["tool"] },
|
|
419
|
+
popio: {
|
|
420
|
+
type: "remote",
|
|
421
|
+
url: "http://127.0.0.1:9999/mcp",
|
|
422
|
+
headers: { Authorization: "Bearer fake-token" },
|
|
423
|
+
enabled: true,
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
opts.onEvent?.({
|
|
428
|
+
harness: "opencode",
|
|
429
|
+
seq: 0,
|
|
430
|
+
at: Date.now(),
|
|
431
|
+
raw: { type: "result", info: { costUsd: 0.13 } },
|
|
432
|
+
type: "raw",
|
|
433
|
+
});
|
|
434
|
+
return makeFakeHarnessRun(makeSuccessResult());
|
|
435
|
+
});
|
|
436
|
+
const startMcpIoServer = mock(async () => createFakeMcpHandle());
|
|
437
|
+
|
|
438
|
+
const result = await executeAgent(
|
|
439
|
+
{ io, entry: { name: "a", harness: "opencode", prompt: "p" } },
|
|
440
|
+
{ run, startMcpIoServer },
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
expect(capturedMcpServers).toBeUndefined();
|
|
444
|
+
expect(result.costUsd).toBe(0.13);
|
|
445
|
+
expect(process.env.OPENCODE_CONFIG_DIR).toBe(sourceConfigDir);
|
|
446
|
+
expect(existsSync(configDir)).toBe(false);
|
|
447
|
+
} finally {
|
|
448
|
+
rmSync(sourceConfigDir, { recursive: true, force: true });
|
|
449
|
+
if (previousConfigDir === undefined) {
|
|
450
|
+
delete process.env.OPENCODE_CONFIG_DIR;
|
|
451
|
+
} else {
|
|
452
|
+
process.env.OPENCODE_CONFIG_DIR = previousConfigDir;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("preserves usage and cost when artifact writes fail after the harness run", async () => {
|
|
458
|
+
const io = createFakeIO();
|
|
459
|
+
io.writeArtifact = async () => {
|
|
460
|
+
throw new Error("disk full");
|
|
461
|
+
};
|
|
462
|
+
const run = mock(() => makeFakeHarnessRun(makeSuccessResult({ costUsd: 0.21 })));
|
|
463
|
+
const startMcpIoServer = mock(async () => createFakeMcpHandle(["agent-output.md"]));
|
|
464
|
+
|
|
465
|
+
const result = await executeAgent(
|
|
466
|
+
{ io, entry: { name: "a", harness: "claude", prompt: "p" } },
|
|
467
|
+
{ run, startMcpIoServer },
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(result.ok).toBe(false);
|
|
471
|
+
expect(result.error).toBe("disk full");
|
|
472
|
+
expect(result.usage).toEqual({ inputTokens: 100, outputTokens: 50, totalTokens: 150 });
|
|
473
|
+
expect(result.costUsd).toBe(0.21);
|
|
474
|
+
expect(result.sessionId).toBe("sess-1");
|
|
475
|
+
expect(result.artifactsWritten).toContain("agent-output.md");
|
|
476
|
+
});
|
|
181
477
|
});
|
|
182
478
|
|
|
183
479
|
describe("runAgentStep", () => {
|
|
@@ -293,16 +589,39 @@ describe("runAgentStep", () => {
|
|
|
293
589
|
it("MCP server is closed on timeout path", async () => {
|
|
294
590
|
const mcpHandle = createFakeMcpHandle();
|
|
295
591
|
const createTaskFileIO = mock(() => createFakeIO());
|
|
592
|
+
let capturedTimeoutMs: number | undefined;
|
|
593
|
+
let capturedIdleTimeoutMs: number | undefined;
|
|
296
594
|
const deps = {
|
|
297
|
-
run: mock(() =>
|
|
595
|
+
run: mock((opts) => {
|
|
596
|
+
capturedTimeoutMs = opts.timeoutMs;
|
|
597
|
+
capturedIdleTimeoutMs = opts.idleTimeoutMs;
|
|
598
|
+
return makeFakeHarnessRun(new Error('Harness "claude" timed out after 100ms'));
|
|
599
|
+
}),
|
|
298
600
|
startMcpIoServer: mock(async () => mcpHandle),
|
|
299
601
|
createTaskFileIO,
|
|
300
602
|
};
|
|
301
603
|
|
|
302
604
|
await runAgentStep(makeArgs({ entry: { timeoutMs: 100 } }), deps);
|
|
605
|
+
expect(capturedTimeoutMs).toBe(100);
|
|
606
|
+
expect(capturedIdleTimeoutMs).toBeUndefined();
|
|
303
607
|
expect(mcpHandle.closeSpy).toHaveBeenCalled();
|
|
304
608
|
});
|
|
305
609
|
|
|
610
|
+
it("passes an explicit idle timeout through to the adapter", async () => {
|
|
611
|
+
let capturedIdleTimeoutMs: number | undefined;
|
|
612
|
+
const deps = {
|
|
613
|
+
run: mock((opts) => {
|
|
614
|
+
capturedIdleTimeoutMs = opts.idleTimeoutMs;
|
|
615
|
+
return makeFakeHarnessRun(makeSuccessResult());
|
|
616
|
+
}),
|
|
617
|
+
startMcpIoServer: mock(async () => createFakeMcpHandle()),
|
|
618
|
+
createTaskFileIO: mock(() => createFakeIO()),
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
await runAgentStep(makeArgs({ entry: { idleTimeoutMs: 250 } }), deps);
|
|
622
|
+
expect(capturedIdleTimeoutMs).toBe(250);
|
|
623
|
+
});
|
|
624
|
+
|
|
306
625
|
it("does not start MCP server when io is false", async () => {
|
|
307
626
|
const createTaskFileIO = mock(() => createFakeIO());
|
|
308
627
|
const deps = {
|
|
@@ -245,7 +245,13 @@ describe("runPipeline runAgent injection", () => {
|
|
|
245
245
|
llm: {} as never,
|
|
246
246
|
runAgent: async (options) => {
|
|
247
247
|
calls.push(options);
|
|
248
|
-
return {
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
finalMessage: "did it",
|
|
251
|
+
artifactsWritten: ["x.md"],
|
|
252
|
+
usage: { inputTokens: 12, outputTokens: 7, totalTokens: 19 },
|
|
253
|
+
costUsd: 0.03,
|
|
254
|
+
};
|
|
249
255
|
},
|
|
250
256
|
});
|
|
251
257
|
|
|
@@ -257,5 +263,55 @@ describe("runPipeline runAgent injection", () => {
|
|
|
257
263
|
finalMessage: "did it",
|
|
258
264
|
});
|
|
259
265
|
}
|
|
266
|
+
|
|
267
|
+
const status = JSON.parse(await readFile(path.join(workDir, "tasks-status.json"), "utf8")) as StatusSnapshot;
|
|
268
|
+
expect(status.tasks["agent-task"]?.tokenUsage).toEqual([
|
|
269
|
+
["claude:default", 12, 7, 0.03],
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("records runAgent cost when normalized usage is absent", async () => {
|
|
274
|
+
const root = await makeTempRoot();
|
|
275
|
+
const workDir = path.join(root, "job-agent-cost");
|
|
276
|
+
await mkdir(workDir, { recursive: true });
|
|
277
|
+
await writeFile(
|
|
278
|
+
path.join(workDir, "tasks-status.json"),
|
|
279
|
+
JSON.stringify({ id: "job-agent-cost", tasks: {} }),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const modulePath = path.join(root, "agent-cost-task.mjs");
|
|
283
|
+
await writeFile(
|
|
284
|
+
modulePath,
|
|
285
|
+
[
|
|
286
|
+
"export const ingestion = async ({ runAgent, flags }) => {",
|
|
287
|
+
" const r = await runAgent({ harness: 'opencode', model: 'gpt-4o', prompt: 'hello agent' });",
|
|
288
|
+
" return { output: r, flags };",
|
|
289
|
+
"};",
|
|
290
|
+
].join("\n"),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const result = await runPipeline(modulePath, {
|
|
294
|
+
workDir,
|
|
295
|
+
taskName: "agent-cost-task",
|
|
296
|
+
statusPath: path.join(workDir, "tasks-status.json"),
|
|
297
|
+
jobId: "job-agent-cost",
|
|
298
|
+
envLoaded: true,
|
|
299
|
+
seed: { data: {} },
|
|
300
|
+
pipelineTasks: ["agent-cost-task"],
|
|
301
|
+
llm: {} as never,
|
|
302
|
+
runAgent: async () => ({
|
|
303
|
+
ok: true,
|
|
304
|
+
finalMessage: "did it",
|
|
305
|
+
artifactsWritten: [],
|
|
306
|
+
costUsd: 0.12,
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(result.ok).toBe(true);
|
|
311
|
+
|
|
312
|
+
const status = JSON.parse(await readFile(path.join(workDir, "tasks-status.json"), "utf8")) as StatusSnapshot;
|
|
313
|
+
expect(status.tasks["agent-cost-task"]?.tokenUsage).toEqual([
|
|
314
|
+
["opencode:gpt-4o", 0, 0, 0.12],
|
|
315
|
+
]);
|
|
260
316
|
});
|
|
261
317
|
});
|