@selesai/code 0.5.29 → 0.6.1
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/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +18 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/system-prompt.test.d.ts +2 -0
- package/dist/core/system-prompt.test.d.ts.map +1 -0
- package/dist/core/system-prompt.test.js +89 -0
- package/dist/core/system-prompt.test.js.map +1 -0
- package/dist/defaults/models.json +13 -45
- package/dist/defaults/settings.json +1 -2
- package/dist/extensions/copy-turn.test.ts +131 -0
- package/dist/extensions/copy-turn.ts +6 -1
- package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/dist/extensions/package.json +0 -1
- package/dist/extensions/pi-subagents/CHANGELOG.md +3 -0
- package/dist/extensions/pi-subagents/README.md +27 -32
- package/dist/extensions/pi-subagents/agents/architect.md +4 -4
- package/dist/extensions/pi-subagents/agents/builder.md +5 -4
- package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
- package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
- package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +10 -9
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +12 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +10 -11
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +56 -9
- package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +24 -7
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +23 -5
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +27 -1
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +64 -6
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +16 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +72 -18
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +19 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +127 -31
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +4 -6
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +41 -2
- package/dist/extensions/pi-subagents/src/shared/utils.ts +29 -1
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -1
- package/dist/extensions/pi-subagents/src/tui/render.ts +28 -6
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +111 -6
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +74 -43
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +36 -21
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +5 -3
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -8
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +14 -7
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +81 -5
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +49 -10
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
- package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +29 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
- package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +23 -1
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +60 -9
- package/dist/skills/ponytail/SKILL.md +1 -3
- package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
- package/package.json +2 -2
- package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
- package/dist/extensions/caveman/index.js +0 -118
- package/dist/extensions/caveman/package.json +0 -8
- package/dist/extensions/caveman/test/extension.test.js +0 -203
- package/dist/extensions/caveman/test/helpers.test.js +0 -58
- package/dist/skills/caveman/SKILL.md +0 -50
|
@@ -80,7 +80,7 @@ interface AsyncResultPayload {
|
|
|
80
80
|
totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
|
|
81
81
|
usageBudget?: UsageBudgetState;
|
|
82
82
|
checkpoint?: { name?: string; status?: string };
|
|
83
|
-
results: Array<{ agent?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
|
|
83
|
+
results: Array<{ agent?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputPath?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
|
|
84
84
|
outputs?: Record<string, { text?: string; structured?: unknown }>;
|
|
85
85
|
workflowGraph?: { nodes?: Array<{ kind?: string; label?: string; phase?: string; status?: string; acceptanceStatus?: string; error?: string; outputName?: string; structured?: boolean; children?: Array<{ label?: string; outputName?: string; itemKey?: string; status?: string; acceptanceStatus?: string; error?: string }> }> };
|
|
86
86
|
parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
|
|
@@ -453,6 +453,18 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
453
453
|
return JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Reference-first async child assertion: the result payload carries the
|
|
458
|
+
* saved-output reference; the full text is read from the durable output path.
|
|
459
|
+
*/
|
|
460
|
+
function assertAsyncChildOutput(payload: AsyncResultPayload, index: number, expected: string): void {
|
|
461
|
+
const child = payload.results[index];
|
|
462
|
+
assert.ok(child, `expected async child ${index}`);
|
|
463
|
+
assert.match(child.output ?? "", /Output saved to: /);
|
|
464
|
+
assert.ok(child.outputPath, `expected a durable output path for child ${index}`);
|
|
465
|
+
assert.equal(fs.readFileSync(child.outputPath, "utf-8"), expected);
|
|
466
|
+
}
|
|
467
|
+
|
|
456
468
|
function launchProtocolTest(id: string): void {
|
|
457
469
|
executeAsyncSingle(id, {
|
|
458
470
|
agent: "worker",
|
|
@@ -480,7 +492,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
480
492
|
launchProtocolTest(id);
|
|
481
493
|
const payload = await readAsyncPayload(id);
|
|
482
494
|
assert.equal(payload.success, true);
|
|
483
|
-
|
|
495
|
+
assertAsyncChildOutput(payload, 0, "你好 from fragmented async JSON");
|
|
484
496
|
});
|
|
485
497
|
|
|
486
498
|
it("persists absent output provenance when async lifecycle text is synthetic", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -500,7 +512,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
500
512
|
const payload = await readAsyncPayload(id);
|
|
501
513
|
assert.equal(payload.success, false);
|
|
502
514
|
assert.equal(payload.results[0]?.outputState, "present");
|
|
503
|
-
|
|
515
|
+
assertAsyncChildOutput(payload, 0, "usable partial answer");
|
|
504
516
|
});
|
|
505
517
|
|
|
506
518
|
it("matches preflight launch digest in equivalent foreground and async execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -512,11 +524,14 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
512
524
|
fs.writeFileSync(agentPath, `---\nname: ${agentName}\ndescription: Contract comparison worker\n---\n`, "utf-8");
|
|
513
525
|
const discovered = discoverAgents(tempDir).agents.find((agent) => agent.name === agentName);
|
|
514
526
|
assert.ok(discovered, "expected temporary agent definition to be discovered");
|
|
515
|
-
|
|
527
|
+
// Stable explicit output keeps the preflight launch contract equivalent to
|
|
528
|
+
// both execution paths (generated per-run paths would differ by design).
|
|
529
|
+
const contractOutputPath = path.join(tempDir, "contract-output.md");
|
|
530
|
+
const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, turnBudget, runId: "contract-preflight", output: contractOutputPath });
|
|
516
531
|
assert.equal(preflight.ok, true);
|
|
517
532
|
|
|
518
533
|
mockPi.onCall({ output: "foreground contract comparison" });
|
|
519
|
-
const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget });
|
|
534
|
+
const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget, outputPath: contractOutputPath });
|
|
520
535
|
assert.equal(foreground.exitCode, 0);
|
|
521
536
|
assert.equal(foreground.launchContractDigest, preflight.contract.launchContractDigest);
|
|
522
537
|
|
|
@@ -525,6 +540,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
525
540
|
const launch = executeAsyncSingle(asyncId, {
|
|
526
541
|
agent: agentName,
|
|
527
542
|
task,
|
|
543
|
+
output: contractOutputPath,
|
|
544
|
+
outputMode: "inline",
|
|
528
545
|
agentConfig: discovered,
|
|
529
546
|
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
530
547
|
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
@@ -721,7 +738,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
721
738
|
|
|
722
739
|
const launch = await executor.execute(
|
|
723
740
|
"async-session-artifact-dir",
|
|
724
|
-
{ agent: "worker", task: "Write async session artifacts", async: true, runId: "async-session-artifacts", acceptance: false },
|
|
741
|
+
{ agent: "worker", task: "Write async session artifacts", async: true, runId: "async-session-artifacts", acceptance: false, artifacts: true },
|
|
725
742
|
new AbortController().signal,
|
|
726
743
|
undefined,
|
|
727
744
|
ctx,
|
|
@@ -829,7 +846,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
829
846
|
launchProtocolTest(id);
|
|
830
847
|
const payload = await readAsyncPayload(id);
|
|
831
848
|
assert.equal(payload.success, true);
|
|
832
|
-
|
|
849
|
+
assertAsyncChildOutput(payload, 0, "settled async response");
|
|
833
850
|
assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during the retry delay");
|
|
834
851
|
});
|
|
835
852
|
|
|
@@ -841,7 +858,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
841
858
|
const payload = await readAsyncPayload(id);
|
|
842
859
|
assert.equal(payload.success, true);
|
|
843
860
|
assert.equal(payload.results[0]?.error, undefined);
|
|
844
|
-
|
|
861
|
+
assertAsyncChildOutput(payload, 0, "settled async without a terminal assistant stop");
|
|
845
862
|
assert.ok(Date.now() - startedAt < 4000, "agent_settled should trigger bounded child cleanup");
|
|
846
863
|
});
|
|
847
864
|
|
|
@@ -872,7 +889,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
872
889
|
assert.match(call.args.at(-1) ?? "", /\{outputs\.name\}/);
|
|
873
890
|
const payload = await readAsyncPayload(id);
|
|
874
891
|
assert.equal(payload.success, true);
|
|
875
|
-
|
|
892
|
+
assertAsyncChildOutput(payload, 0, "OK");
|
|
876
893
|
});
|
|
877
894
|
|
|
878
895
|
it("spawns the async runner with node when process.execPath is not node", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -903,7 +920,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
903
920
|
const resultPath = await waitForAsyncResultFile(id, 30_000);
|
|
904
921
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
905
922
|
assert.equal(payload.success, true);
|
|
906
|
-
|
|
923
|
+
assertAsyncChildOutput(payload, 0, "non-node exec async done");
|
|
907
924
|
} finally {
|
|
908
925
|
process.execPath = originalExecPath;
|
|
909
926
|
}
|
|
@@ -937,7 +954,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
937
954
|
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
938
955
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
939
956
|
assert.equal(payload.success, true);
|
|
940
|
-
|
|
957
|
+
assertAsyncChildOutput(payload, 0, "stale node exec async done");
|
|
941
958
|
} finally {
|
|
942
959
|
process.execPath = originalExecPath;
|
|
943
960
|
}
|
|
@@ -1178,8 +1195,11 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1178
1195
|
assert.equal(payload.turnBudget?.turnCount, 2);
|
|
1179
1196
|
assert.equal(payload.results[0]?.wrapUpRequested, true);
|
|
1180
1197
|
assert.equal(payload.results[0]?.turnBudget?.turnCount, 2);
|
|
1181
|
-
|
|
1182
|
-
|
|
1198
|
+
// Reference-first delivery: the saved-output reference replaces inline prose;
|
|
1199
|
+
// the wrap-up note and raw output stay visible through status/result fields
|
|
1200
|
+
// and the persisted result file.
|
|
1201
|
+
assert.match(payload.results[0]?.output ?? "", /Output saved to: /);
|
|
1202
|
+
assertAsyncChildOutput(payload, 0, "final wrapped output");
|
|
1183
1203
|
assert.equal(status.wrapUpRequested, true);
|
|
1184
1204
|
assert.equal(status.turnBudgetExceeded, undefined);
|
|
1185
1205
|
assert.equal(status.steps?.[0]?.wrapUpRequested, true);
|
|
@@ -1218,8 +1238,9 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1218
1238
|
assert.equal(payload.turnBudget?.turnCount, 3);
|
|
1219
1239
|
assert.equal(payload.turnBudget?.exceededAtTurn, 3);
|
|
1220
1240
|
assert.equal(payload.results[0]?.turnBudgetExceeded, true);
|
|
1221
|
-
assert.match(payload.
|
|
1222
|
-
assert.match(payload.results[0]?.output ?? "", /
|
|
1241
|
+
assert.match(payload.error ?? "", /Subagent exceeded turn budget|turn budget/i);
|
|
1242
|
+
assert.match(payload.results[0]?.output ?? "", /Output saved to: /);
|
|
1243
|
+
assertAsyncChildOutput(payload, 0, "safe assistant boundary after tool work");
|
|
1223
1244
|
assert.equal(status.state, "failed");
|
|
1224
1245
|
assert.equal(status.turnBudgetExceeded, true);
|
|
1225
1246
|
assert.equal(status.steps?.[0]?.turnBudgetExceeded, true);
|
|
@@ -1272,7 +1293,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1272
1293
|
assert.equal(payload.turnBudget?.outcome, "exceeded");
|
|
1273
1294
|
assert.equal(payload.turnBudget?.turnCount, 2);
|
|
1274
1295
|
assert.equal(payload.results[0]?.turnBudgetExceeded, true);
|
|
1275
|
-
assert.match(payload.results[0]?.output ?? "", /
|
|
1296
|
+
assert.match(payload.results[0]?.output ?? "", /Output saved to: /);
|
|
1297
|
+
assertAsyncChildOutput(payload, 0, "safe assistant boundary reached");
|
|
1276
1298
|
assert.equal(status.state, "failed");
|
|
1277
1299
|
assert.equal(status.turnBudgetExceeded, true);
|
|
1278
1300
|
assert.equal(status.steps?.[0]?.turnBudget?.outcome, "exceeded");
|
|
@@ -1564,6 +1586,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1564
1586
|
tasks: [{ agent: "builder", task: "Do async work", output: "async-top-output.md", reads: ["input.md"] }],
|
|
1565
1587
|
async: true,
|
|
1566
1588
|
clarify: false,
|
|
1589
|
+
artifacts: true,
|
|
1567
1590
|
},
|
|
1568
1591
|
new AbortController().signal,
|
|
1569
1592
|
undefined,
|
|
@@ -1622,7 +1645,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1622
1645
|
];
|
|
1623
1646
|
const launch = await executor.execute(
|
|
1624
1647
|
`async-inherited-output-${outputOverride === true ? "true" : "omitted"}`,
|
|
1625
|
-
{ tasks, async: true, clarify: false },
|
|
1648
|
+
{ tasks, async: true, clarify: false, artifacts: true },
|
|
1626
1649
|
new AbortController().signal,
|
|
1627
1650
|
undefined,
|
|
1628
1651
|
makeMinimalCtx(tempDir),
|
|
@@ -1631,8 +1654,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1631
1654
|
assert.equal(launch.isError, undefined);
|
|
1632
1655
|
const payload = await readAsyncPayload(launch.details?.asyncId as string);
|
|
1633
1656
|
assert.equal(payload.success, true);
|
|
1634
|
-
|
|
1635
|
-
|
|
1657
|
+
assertAsyncChildOutput(payload, 0, "first async report");
|
|
1658
|
+
assertAsyncChildOutput(payload, 1, "second async report");
|
|
1636
1659
|
const outputDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", launch.details?.asyncId as string);
|
|
1637
1660
|
const authoritativePaths = [
|
|
1638
1661
|
path.join(outputDir, "parallel-0", "0-worker", "context.md"),
|
|
@@ -1694,6 +1717,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1694
1717
|
],
|
|
1695
1718
|
async: true,
|
|
1696
1719
|
clarify: false,
|
|
1720
|
+
artifacts: true,
|
|
1697
1721
|
},
|
|
1698
1722
|
new AbortController().signal,
|
|
1699
1723
|
undefined,
|
|
@@ -1932,13 +1956,13 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1932
1956
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
1933
1957
|
const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
|
|
1934
1958
|
assert.equal(payload.success, true);
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1959
|
+
// Reference-first chain delivery: every child result carries the saved-output
|
|
1960
|
+
// reference; full text is read from each durable output path.
|
|
1961
|
+
assertAsyncChildOutput(payload, 0, "Scout A async findings");
|
|
1962
|
+
assertAsyncChildOutput(payload, 1, "Scout B async findings");
|
|
1963
|
+
assertAsyncChildOutput(payload, 2, "Async funnel synthesis");
|
|
1964
|
+
assertAsyncChildOutput(payload, 3, "Async reviewer A done");
|
|
1965
|
+
assertAsyncChildOutput(payload, 4, "Async reviewer B done");
|
|
1942
1966
|
assert.deepEqual(status.steps?.map((step) => step.status), ["complete", "complete", "complete", "complete", "complete"]);
|
|
1943
1967
|
assert.deepEqual(status.parallelGroups, [
|
|
1944
1968
|
{ start: 0, count: 2, stepIndex: 0 },
|
|
@@ -1946,11 +1970,14 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1946
1970
|
]);
|
|
1947
1971
|
const funnelTask = readMockPiArgsMatching(mockPi, "Synthesize:").at(-1) ?? "";
|
|
1948
1972
|
assert.match(funnelTask, /=== Parallel Task 1 \(scout-a\) ===/);
|
|
1949
|
-
assert.match(funnelTask, /Scout A async findings/);
|
|
1950
1973
|
assert.match(funnelTask, /=== Parallel Task 2 \(scout-b\) ===/);
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
assert.match(
|
|
1974
|
+
// Chain handoff stays reference-first: the funnel consumes the saved-output
|
|
1975
|
+
// references and reads the named paths instead of re-inlined child prose.
|
|
1976
|
+
assert.match(funnelTask, /Output saved to: /);
|
|
1977
|
+
assert.doesNotMatch(funnelTask, /Scout A async findings/);
|
|
1978
|
+
assert.doesNotMatch(funnelTask, /Scout B async findings/);
|
|
1979
|
+
assert.match(readMockPiArgsMatching(mockPi, "Review funnel A:").at(-1) ?? "", /Review funnel A:\nOutput saved to: /);
|
|
1980
|
+
assert.match(readMockPiArgsMatching(mockPi, "Review funnel B:").at(-1) ?? "", /Review funnel B:\nOutput saved to: /);
|
|
1954
1981
|
assert.equal(payload.workflowGraph?.nodes?.[0]?.kind, "parallel-group");
|
|
1955
1982
|
assert.equal(payload.workflowGraph?.nodes?.[0]?.status, "completed");
|
|
1956
1983
|
assert.equal(payload.workflowGraph?.nodes?.[1]?.kind, "step");
|
|
@@ -2341,7 +2368,10 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2341
2368
|
const expectedConsumerTarget = `subagent-consumer-${id}-4`;
|
|
2342
2369
|
assert.equal(payload.success, true);
|
|
2343
2370
|
assert.equal(payload.results[3]?.intercomTarget, expectedConsumerTarget);
|
|
2344
|
-
|
|
2371
|
+
const consumerChild = payload.results[3];
|
|
2372
|
+
assert.ok(consumerChild?.outputPath, "expected a durable output path for the consumer child");
|
|
2373
|
+
assert.match(consumerChild.output ?? "", /Output saved to: /);
|
|
2374
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(consumerChild.outputPath, "utf-8")), { SELESAI_SUBAGENT_INTERCOM_SESSION_NAME: expectedConsumerTarget });
|
|
2345
2375
|
});
|
|
2346
2376
|
|
|
2347
2377
|
it("async dynamic pre-spawn failures persist failed graph status and error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -2428,6 +2458,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2428
2458
|
async: true,
|
|
2429
2459
|
clarify: false,
|
|
2430
2460
|
worktree: true,
|
|
2461
|
+
artifacts: true,
|
|
2431
2462
|
},
|
|
2432
2463
|
new AbortController().signal,
|
|
2433
2464
|
undefined,
|
|
@@ -2632,7 +2663,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2632
2663
|
assert.equal(payload.results[0]?.model, "openai/gpt-5-mini:high");
|
|
2633
2664
|
assert.deepEqual(payload.results[0]?.attemptedModels, ["openai/gpt-5-mini:high"]);
|
|
2634
2665
|
assert.deepEqual(payload.results[0]?.modelAttempts?.map((attempt) => attempt.success), [false, true]);
|
|
2635
|
-
|
|
2666
|
+
assertAsyncChildOutput(payload, 0, "Recovered asynchronously after startup race");
|
|
2636
2667
|
assert.equal(mockPi.callCount(), 2);
|
|
2637
2668
|
});
|
|
2638
2669
|
|
|
@@ -2679,7 +2710,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2679
2710
|
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
|
|
2680
2711
|
assert.equal(payload.success, true);
|
|
2681
2712
|
assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high", "anthropic/claude-sonnet-4:low"]);
|
|
2682
|
-
|
|
2713
|
+
assertAsyncChildOutput(payload, 0, "Recovered after stream failure");
|
|
2683
2714
|
assert.equal(mockPi.callCount(), 2);
|
|
2684
2715
|
});
|
|
2685
2716
|
|
|
@@ -2820,7 +2851,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2820
2851
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
2821
2852
|
assert.equal(payload.success, true);
|
|
2822
2853
|
assert.equal(payload.results[0]?.model, "anthropic/claude-sonnet-4");
|
|
2823
|
-
|
|
2854
|
+
assertAsyncChildOutput(payload, 0, "Recovered asynchronously from empty output");
|
|
2824
2855
|
assert.match(payload.results[0]?.modelAttempts?.[0]?.error ?? "", /no output/i);
|
|
2825
2856
|
assert.deepEqual(payload.results[0]?.modelAttempts?.map((attempt) => attempt.success), [false, true]);
|
|
2826
2857
|
assert.equal(mockPi.callCount(), 2);
|
|
@@ -2960,7 +2991,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2960
2991
|
assert.equal(payload.exitCode, 0);
|
|
2961
2992
|
assert.equal(payload.results[0]?.success, true);
|
|
2962
2993
|
assert.equal(payload.results[0]?.error, undefined);
|
|
2963
|
-
|
|
2994
|
+
assertAsyncChildOutput(payload, 0, "Recovered asynchronously");
|
|
2964
2995
|
const statusPayload = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload;
|
|
2965
2996
|
assert.equal(statusPayload.state, "complete");
|
|
2966
2997
|
assert.equal(statusPayload.steps?.[0]?.status, "complete");
|
|
@@ -3364,7 +3395,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3364
3395
|
assert.equal(payload.success, true);
|
|
3365
3396
|
assert.equal(payload.exitCode, 0);
|
|
3366
3397
|
assert.equal(payload.results[0].success, true);
|
|
3367
|
-
|
|
3398
|
+
assertAsyncChildOutput(payload, 0, "cold start test after patch");
|
|
3368
3399
|
|
|
3369
3400
|
const eventsPath = path.join(ASYNC_DIR, id, "events.jsonl");
|
|
3370
3401
|
const eventsText = fs.readFileSync(eventsPath, "utf-8");
|
|
@@ -4070,7 +4101,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4070
4101
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
4071
4102
|
assert.ok(elapsed < 6000, `unconfigured watchdog status should not delay async final drain, took ${elapsed}ms`);
|
|
4072
4103
|
assert.equal(payload.success, true);
|
|
4073
|
-
|
|
4104
|
+
assertAsyncChildOutput(payload, 0, "async-done-without-watchdog-config");
|
|
4074
4105
|
assert.equal((payload.results[0] as { watchdog?: unknown }).watchdog, undefined);
|
|
4075
4106
|
});
|
|
4076
4107
|
});
|
|
@@ -4105,7 +4136,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4105
4136
|
assert.ok(elapsed >= 1200, `watchdog settlement should delay async final drain, took ${elapsed}ms`);
|
|
4106
4137
|
assert.ok(elapsed < 9000, `settled watchdog should still allow async cleanup, took ${elapsed}ms`);
|
|
4107
4138
|
assert.equal(payload.success, true);
|
|
4108
|
-
|
|
4139
|
+
assertAsyncChildOutput(payload, 0, "async-done-before-watchdog");
|
|
4109
4140
|
assert.equal((payload.results[0] as { watchdog?: { phase?: string } }).watchdog?.phase, "idle");
|
|
4110
4141
|
});
|
|
4111
4142
|
});
|
|
@@ -4136,7 +4167,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4136
4167
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
4137
4168
|
assert.ok(elapsed < 6000, `watchdog tail fallback should not hang async final drain, took ${elapsed}ms`);
|
|
4138
4169
|
assert.equal(payload.success, true);
|
|
4139
|
-
|
|
4170
|
+
assertAsyncChildOutput(payload, 0, "async-done-before-watchdog-timeout");
|
|
4140
4171
|
const watchdog = (payload.results[0] as { watchdog?: { phase?: string; timedOut?: boolean } }).watchdog;
|
|
4141
4172
|
assert.equal(watchdog?.phase, "stale");
|
|
4142
4173
|
assert.equal(watchdog?.timedOut, true);
|
|
@@ -4187,7 +4218,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4187
4218
|
assert.equal(payload.success, true);
|
|
4188
4219
|
assert.equal(payload.exitCode, 0);
|
|
4189
4220
|
assert.equal(payload.results[0].success, true);
|
|
4190
|
-
|
|
4221
|
+
assertAsyncChildOutput(payload, 0, "async-done-before-drain");
|
|
4191
4222
|
});
|
|
4192
4223
|
|
|
4193
4224
|
it("background forced drain after empty terminal assistant output is cleanup success", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -4223,7 +4254,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4223
4254
|
assert.equal(payload.success, true);
|
|
4224
4255
|
assert.equal(payload.exitCode, 0);
|
|
4225
4256
|
assert.equal(payload.results[0].success, true);
|
|
4226
|
-
assert.
|
|
4257
|
+
assert.match(payload.results[0].output ?? "", /Output saved to: /);
|
|
4227
4258
|
});
|
|
4228
4259
|
|
|
4229
4260
|
it("background final-drain cleanup preserves explicit assistant errors", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -4596,7 +4627,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4596
4627
|
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
4597
4628
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
4598
4629
|
assert.equal(payload.success, true);
|
|
4599
|
-
|
|
4630
|
+
assertAsyncChildOutput(payload, 0, "Done after noisy stream");
|
|
4600
4631
|
|
|
4601
4632
|
const eventsText = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8");
|
|
4602
4633
|
assert.doesNotMatch(eventsText, /"type":"message_update"/);
|
|
@@ -4675,7 +4706,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4675
4706
|
|
|
4676
4707
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
4677
4708
|
assert.equal(payload.success, true);
|
|
4678
|
-
|
|
4709
|
+
assertAsyncChildOutput(payload, 0, "Done streaming");
|
|
4679
4710
|
|
|
4680
4711
|
const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8"));
|
|
4681
4712
|
assert.deepEqual(status.steps[0].recentTools.map((tool: { tool: string; args: string }) => ({ tool: tool.tool, args: tool.args })), [{ tool: "bash", args: "ls" }]);
|
|
@@ -367,8 +367,9 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
367
367
|
assert.doesNotMatch(firstTaskArg, /\[Write to:|Write your findings to exactly this path/);
|
|
368
368
|
assert.ok(result.details.results[0]?.savedOutputPath);
|
|
369
369
|
assert.equal(fs.readFileSync(result.details.results[0].savedOutputPath, "utf-8"), "full chain output\nwith details");
|
|
370
|
-
assert.
|
|
371
|
-
assert.
|
|
370
|
+
assert.equal(result.details.results[0]?.finalOutput, undefined);
|
|
371
|
+
assert.match(result.details.results[0]?.outputReference?.message ?? "", /Output saved to:/);
|
|
372
|
+
assert.doesNotMatch(result.details.results[0]?.outputReference?.message ?? "", /full chain output/);
|
|
372
373
|
const secondTaskArg = readCallArgs(1).at(-1) ?? "";
|
|
373
374
|
assert.match(secondTaskArg, /Output saved to:/);
|
|
374
375
|
assert.match(secondTaskArg, /2 lines/);
|
|
@@ -404,7 +405,8 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
404
405
|
);
|
|
405
406
|
|
|
406
407
|
assert.ok(!result.isError, `chain should succeed: ${JSON.stringify(result.content)}`);
|
|
407
|
-
assert.
|
|
408
|
+
assert.equal(result.details.results[0]?.finalOutput, undefined);
|
|
409
|
+
assert.match(result.details.results[0]?.outputReference?.message ?? "", /Output saved to:/);
|
|
408
410
|
assert.equal(result.details.results[0]?.acceptance?.status, "checked");
|
|
409
411
|
assert.ok(result.details.results[0]?.acceptance?.childReport);
|
|
410
412
|
|
|
@@ -630,10 +632,13 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
630
632
|
|
|
631
633
|
assert.ok(!result.isError);
|
|
632
634
|
const step2Task = result.details.results[1].task;
|
|
635
|
+
// Reference-first handoff: {previous} carries the saved-output reference and
|
|
636
|
+
// the step reads the named path instead of re-inlined prose.
|
|
633
637
|
assert.ok(
|
|
634
|
-
step2Task.includes("
|
|
635
|
-
`step 2 task should contain
|
|
638
|
+
step2Task.includes("Output saved to:"),
|
|
639
|
+
`step 2 task should contain the saved-output reference via {previous}: ${step2Task.slice(0, 200)}`,
|
|
636
640
|
);
|
|
641
|
+
assert.doesNotMatch(step2Task, /MARKER_ABC_123/);
|
|
637
642
|
});
|
|
638
643
|
|
|
639
644
|
it("passes named sequential outputs through {outputs.name}", async () => {
|
|
@@ -652,7 +657,8 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
652
657
|
);
|
|
653
658
|
|
|
654
659
|
assert.ok(!result.isError);
|
|
655
|
-
assert.match(readCallArgs(1).at(-1) ?? "", /
|
|
660
|
+
assert.match(readCallArgs(1).at(-1) ?? "", /Output saved to: /);
|
|
661
|
+
assert.doesNotMatch(readCallArgs(1).at(-1) ?? "", /CTX_123/);
|
|
656
662
|
assert.equal(result.details.workflowGraph?.nodes[0]?.outputName, "contextOutput");
|
|
657
663
|
});
|
|
658
664
|
|
|
@@ -692,9 +698,10 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
692
698
|
assert.match(readCallArgs(1).at(-1) ?? "", /Review src\/a\.ts/);
|
|
693
699
|
assert.match(readCallArgs(2).at(-1) ?? "", /Review src\/b\.ts/);
|
|
694
700
|
assert.match(readCallArgs(3).at(-1) ?? "", /"key":"src\/a\.ts"/);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
assert.
|
|
701
|
+
// Terminal details strip chain outputs text/structured; bindings remain
|
|
702
|
+
// reference-first via {outputs.name} and the workflow graph keeps item keys.
|
|
703
|
+
assert.equal(result.details.outputs?.reviews?.text, "");
|
|
704
|
+
assert.equal(result.details.outputs?.reviews?.structured, undefined);
|
|
698
705
|
const dynamicNode = result.details.workflowGraph?.nodes[1];
|
|
699
706
|
assert.equal(dynamicNode?.kind, "dynamic-parallel-group");
|
|
700
707
|
assert.deepEqual(dynamicNode?.children?.map((child) => child.itemKey), ["src/a.ts", "src/b.ts"]);
|
|
@@ -868,7 +875,7 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
868
875
|
{ agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
|
|
869
876
|
{
|
|
870
877
|
expand: { from: { output: "targets", path: "/items" }, key: "/path", maxItems: 4 },
|
|
871
|
-
parallel: { agent: "reviewer", task: "Review {item.path}", outputMode: "file-only" },
|
|
878
|
+
parallel: { agent: "reviewer", task: "Review {item.path}", output: false, outputMode: "file-only" },
|
|
872
879
|
collect: { as: "reviews" },
|
|
873
880
|
},
|
|
874
881
|
],
|
|
@@ -906,7 +913,8 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
906
913
|
|
|
907
914
|
assert.ok(!result.isError, `chain should succeed: ${JSON.stringify(result.content)}`);
|
|
908
915
|
assert.equal(mockPi.callCount(), 2);
|
|
909
|
-
assert.
|
|
916
|
+
assert.equal(result.details.outputs?.reviews?.text, "");
|
|
917
|
+
assert.equal(result.details.outputs?.reviews?.structured, undefined);
|
|
910
918
|
assert.equal(result.details.workflowGraph?.nodes[1]?.status, "completed");
|
|
911
919
|
assert.deepEqual(result.details.workflowGraph?.nodes[1]?.children, []);
|
|
912
920
|
});
|
|
@@ -1123,7 +1131,9 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
1123
1131
|
assert.equal(result.details.results.length, 2);
|
|
1124
1132
|
assert.equal(result.details.results[0]?.acceptance?.status, "rejected");
|
|
1125
1133
|
assert.equal(result.details.results[0]?.exitCode, 0);
|
|
1126
|
-
assert.
|
|
1134
|
+
assert.equal(result.details.results[1]?.finalOutput, undefined);
|
|
1135
|
+
assert.ok(result.details.results[1]?.savedOutputPath);
|
|
1136
|
+
assert.equal(fs.readFileSync(result.details.results[1]!.savedOutputPath!, "utf-8"), "Step 2 ran");
|
|
1127
1137
|
});
|
|
1128
1138
|
|
|
1129
1139
|
it("agent contract v1 chain can gate progression on acceptance", async () => {
|
|
@@ -1195,7 +1205,8 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
1195
1205
|
);
|
|
1196
1206
|
|
|
1197
1207
|
const finalTaskArg = readCallArgs(chainLength - 1).at(-1) ?? "";
|
|
1198
|
-
assert.match(finalTaskArg, /
|
|
1208
|
+
assert.match(finalTaskArg, /Output saved to: /);
|
|
1209
|
+
assert.doesNotMatch(finalTaskArg, /step-38-output/);
|
|
1199
1210
|
assert.doesNotMatch(finalTaskArg, /step-37-output/);
|
|
1200
1211
|
assert.match(result.content[0]?.text ?? "", /40 steps/);
|
|
1201
1212
|
});
|
|
@@ -1284,7 +1295,9 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
1284
1295
|
);
|
|
1285
1296
|
|
|
1286
1297
|
assert.ok(!result.isError);
|
|
1287
|
-
|
|
1298
|
+
const depthChild = result.details.results[0];
|
|
1299
|
+
assert.ok(depthChild.savedOutputPath);
|
|
1300
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(depthChild.savedOutputPath, "utf-8")), {
|
|
1288
1301
|
SELESAI_SUBAGENT_DEPTH: "1",
|
|
1289
1302
|
SELESAI_SUBAGENT_MAX_DEPTH: "1",
|
|
1290
1303
|
});
|
|
@@ -1474,8 +1487,9 @@ describe("chain execution — parallel steps", { skip: !available ? "pi packages
|
|
|
1474
1487
|
|
|
1475
1488
|
assert.ok(!result.isError);
|
|
1476
1489
|
const finalTask = readCallArgs(2).at(-1) ?? "";
|
|
1477
|
-
assert.match(finalTask, /
|
|
1478
|
-
assert.
|
|
1490
|
+
assert.match(finalTask, /Output saved to: /);
|
|
1491
|
+
assert.doesNotMatch(finalTask, /Alpha named output/);
|
|
1492
|
+
assert.doesNotMatch(finalTask, /Beta named output/);
|
|
1479
1493
|
});
|
|
1480
1494
|
|
|
1481
1495
|
it("funnels an initial parallel step through one agent, then fans the funnel output back out", async () => {
|
|
@@ -1512,13 +1526,14 @@ describe("chain execution — parallel steps", { skip: !available ? "pi packages
|
|
|
1512
1526
|
assert.equal(result.details.totalSteps, 3);
|
|
1513
1527
|
const funnelTask = readCallArgsMatching("Synthesize:").at(-1) ?? "";
|
|
1514
1528
|
assert.match(funnelTask, /=== Parallel Task 1 \(scout-a\) ===/);
|
|
1515
|
-
assert.match(funnelTask, /Scout A findings/);
|
|
1516
1529
|
assert.match(funnelTask, /=== Parallel Task 2 \(scout-b\) ===/);
|
|
1517
|
-
assert.match(funnelTask, /
|
|
1530
|
+
assert.match(funnelTask, /Output saved to: /);
|
|
1531
|
+
assert.doesNotMatch(funnelTask, /Scout A findings/);
|
|
1532
|
+
assert.doesNotMatch(funnelTask, /Scout B findings/);
|
|
1518
1533
|
const fanoutTaskA = readCallArgsMatching("Review funnel A:").at(-1) ?? "";
|
|
1519
1534
|
const fanoutTaskB = readCallArgsMatching("Review funnel B:").at(-1) ?? "";
|
|
1520
|
-
assert.match(fanoutTaskA, /Review funnel A:\
|
|
1521
|
-
assert.match(fanoutTaskB, /Review funnel B:\
|
|
1535
|
+
assert.match(fanoutTaskA, /Review funnel A:\nOutput saved to: /);
|
|
1536
|
+
assert.match(fanoutTaskB, /Review funnel B:\nOutput saved to: /);
|
|
1522
1537
|
assert.equal(result.details.workflowGraph?.nodes[0]?.kind, "parallel-group");
|
|
1523
1538
|
assert.equal(result.details.workflowGraph?.nodes[1]?.kind, "step");
|
|
1524
1539
|
assert.equal(result.details.workflowGraph?.nodes[2]?.kind, "parallel-group");
|
|
@@ -1560,7 +1575,7 @@ describe("chain execution — parallel steps", { skip: !available ? "pi packages
|
|
|
1560
1575
|
makeChainParams(
|
|
1561
1576
|
[{
|
|
1562
1577
|
parallel: [
|
|
1563
|
-
{ agent: "reviewer-a", task: "Review A", outputMode: "file-only" },
|
|
1578
|
+
{ agent: "reviewer-a", task: "Review A", output: false, outputMode: "file-only" },
|
|
1564
1579
|
{ agent: "reviewer-b", task: "Review B", output: "b.md" },
|
|
1565
1580
|
],
|
|
1566
1581
|
}],
|
|
@@ -310,7 +310,8 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
|
|
|
310
310
|
|
|
311
311
|
assert.equal(result.isError, undefined);
|
|
312
312
|
const args = readCallArgs();
|
|
313
|
-
assert.ok((args.at(-1) ?? "").startsWith("Task: \n\n
|
|
313
|
+
assert.ok((args.at(-1) ?? "").startsWith("Task: \n\n---\n**Output:**"));
|
|
314
|
+
assert.match(args.at(-1) ?? "", /## Acceptance Contract/);
|
|
314
315
|
});
|
|
315
316
|
|
|
316
317
|
it("does not treat top-level agent as single mode when tasks are present", async () => {
|
|
@@ -327,7 +328,8 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
|
|
|
327
328
|
|
|
328
329
|
assert.equal(result.isError, undefined);
|
|
329
330
|
const args = readCallArgs();
|
|
330
|
-
assert.ok((args.at(-1) ?? "").startsWith("Task: parallel task\n\n
|
|
331
|
+
assert.ok((args.at(-1) ?? "").startsWith("Task: parallel task\n\n---\n**Output:**"));
|
|
332
|
+
assert.match(args.at(-1) ?? "", /## Acceptance Contract/);
|
|
331
333
|
});
|
|
332
334
|
|
|
333
335
|
it("uses agent defaultContext fork when launch context is omitted", async () => {
|
|
@@ -1827,7 +1829,7 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
|
|
|
1827
1829
|
);
|
|
1828
1830
|
|
|
1829
1831
|
assert.equal(result.isError, undefined);
|
|
1830
|
-
const args = readAllCallArgs().find((callArgs) => (callArgs.at(-1) ?? "").startsWith(`Task: ${task}\n\n
|
|
1832
|
+
const args = readAllCallArgs().find((callArgs) => (callArgs.at(-1) ?? "").startsWith(`Task: ${task}\n\n---\n**Output:**`));
|
|
1831
1833
|
assert.ok(args, "expected a recorded mock pi call for this test task");
|
|
1832
1834
|
const modelIndex = args.indexOf("--model");
|
|
1833
1835
|
assert.notEqual(modelIndex, -1);
|
|
@@ -227,10 +227,13 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
|
|
|
227
227
|
assert.match(result.content[0]?.text ?? "", /Delivered single subagent result via intercom\./);
|
|
228
228
|
assert.doesNotMatch(result.content[0]?.text ?? "", /Full child output from worker/);
|
|
229
229
|
assert.equal(result.details?.results?.[0]?.finalOutput, undefined);
|
|
230
|
-
|
|
230
|
+
// Reference-first intercom delivery: the child summary is the saved-output
|
|
231
|
+
// reference (error/status first for failed children), never raw child prose.
|
|
232
|
+
assert.match(String(payload.message ?? ""), /Output saved to: /);
|
|
233
|
+
assert.doesNotMatch(String(payload.message ?? ""), /Full child output from worker/);
|
|
231
234
|
});
|
|
232
235
|
|
|
233
|
-
it("
|
|
236
|
+
it("returns the native saved-output receipt when the bridge is inactive", async () => {
|
|
234
237
|
mockPi.onCall({ output: "Legacy foreground output" });
|
|
235
238
|
const { executor, events } = makeExecutor({ bridgeMode: "off" });
|
|
236
239
|
|
|
@@ -243,10 +246,14 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
|
|
|
243
246
|
);
|
|
244
247
|
|
|
245
248
|
assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), false);
|
|
246
|
-
assert.match(result.content[0]?.text ?? "", /
|
|
249
|
+
assert.match(result.content[0]?.text ?? "", /Output saved to: /);
|
|
250
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /Legacy foreground output/);
|
|
251
|
+
const runId = result.details?.runId;
|
|
252
|
+
assert.ok(runId);
|
|
253
|
+
assert.equal(fs.readFileSync(path.join(tempDir, ".pi-subagents", "artifacts", "outputs", runId, "result.md"), "utf-8"), "Legacy foreground output");
|
|
247
254
|
});
|
|
248
255
|
|
|
249
|
-
it("keeps native
|
|
256
|
+
it("keeps the native saved-output receipt without attempting external grouped delivery when disabled", async () => {
|
|
250
257
|
mockPi.onCall({ output: "Native foreground output" });
|
|
251
258
|
const { executor, events } = makeExecutor({ resultDelivery: false });
|
|
252
259
|
|
|
@@ -259,10 +266,11 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
|
|
|
259
266
|
);
|
|
260
267
|
|
|
261
268
|
assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), false);
|
|
262
|
-
assert.match(result.content[0]?.text ?? "", /
|
|
269
|
+
assert.match(result.content[0]?.text ?? "", /Output saved to: /);
|
|
270
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /Native foreground output/);
|
|
263
271
|
});
|
|
264
272
|
|
|
265
|
-
it("falls back to
|
|
273
|
+
it("falls back to the compact native saved-output receipt when grouped delivery is not acknowledged", async () => {
|
|
266
274
|
mockPi.onCall({ output: "Unacknowledged foreground output" });
|
|
267
275
|
const { executor, events } = makeExecutor({ acknowledgeResults: false });
|
|
268
276
|
|
|
@@ -275,7 +283,8 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
|
|
|
275
283
|
);
|
|
276
284
|
|
|
277
285
|
assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), true);
|
|
278
|
-
assert.match(result.content[0]?.text ?? "", /
|
|
286
|
+
assert.match(result.content[0]?.text ?? "", /Output saved to: /);
|
|
287
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /Unacknowledged foreground output/);
|
|
279
288
|
});
|
|
280
289
|
|
|
281
290
|
it("top-level parallel runs emit one grouped event containing all children", async () => {
|
|
@@ -1250,7 +1259,10 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
|
|
|
1250
1259
|
const payload = completion.payload as { success?: boolean; summary?: string };
|
|
1251
1260
|
assert.equal(payload.success, false);
|
|
1252
1261
|
assert.match(payload.summary ?? "", /Acceptance rejected/);
|
|
1253
|
-
|
|
1262
|
+
// Reference-first foreground completion: the recovery summary carries the
|
|
1263
|
+
// saved-output reference (with error/status), never raw child prose.
|
|
1264
|
+
assert.match(payload.summary ?? "", /Output saved to: /);
|
|
1265
|
+
assert.doesNotMatch(payload.summary ?? "", /final answer with rejected acceptance evidence/);
|
|
1254
1266
|
|
|
1255
1267
|
const status = await executor.execute(
|
|
1256
1268
|
"foreground-detached-failed-status",
|