@selesai/code 0.8.6 → 0.8.7
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 +16 -0
- package/dist/core/agent-session.js +13 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/provider-composer.js +17 -4
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/system-prompt.js +2 -8
- package/dist/core/system-prompt.test.js +6 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/schema-prune.d.ts +16 -0
- package/dist/core/tools/schema-prune.js +39 -0
- package/dist/core/tools/schema-prune.test.d.ts +1 -0
- package/dist/core/tools/schema-prune.test.js +111 -0
- package/dist/defaults/settings.json +1 -0
- package/dist/extensions/model-prompt-injector/config.json +10 -0
- package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
- package/dist/extensions/model-prompt-injector/index.ts +151 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
- package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
- package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
- package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/agents/worker.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
- package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
- package/dist/extensions/pi-subagents/docs/missions.md +5 -3
- package/dist/extensions/pi-subagents/docs/models.md +1 -1
- package/dist/extensions/pi-subagents/docs/observability.md +42 -2
- package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
- package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
- package/dist/extensions/pi-subagents/install.mjs +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
- package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
- package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
- package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
- package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
- package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
- package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
- package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
- package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
- package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
- package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
- package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
- package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
- package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
- package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
- package/dist/extensions/question/index.ts +21 -6
- package/dist/extensions/question/question-list.ts +7 -1
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/exp.test.ts +70 -0
- package/dist/extensions/question/tests/exp2.test.ts +59 -0
- package/dist/extensions/question/tests/exp3.test.ts +47 -0
- package/dist/extensions/question/tests/helpers.test.ts +7 -0
- package/dist/extensions/question/tests/question-list.test.ts +140 -0
- package/dist/extensions/question/tests/row-layout.test.ts +13 -0
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
- package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/docs/settings.md +6 -0
- package/package.json +3 -3
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
formatControlIntercomMessage,
|
|
9
9
|
formatControlNoticeMessage,
|
|
10
10
|
resolveControlConfig,
|
|
11
|
+
shouldEmitOpenToolAttention,
|
|
11
12
|
shouldNotifyControlEvent,
|
|
12
13
|
} from "../../src/runs/shared/subagent-control.ts";
|
|
13
14
|
import { nextLongRunningTrigger } from "../../src/runs/shared/long-running-guard.ts";
|
|
@@ -99,6 +100,15 @@ describe("subagent control attention state", () => {
|
|
|
99
100
|
}), "time_threshold");
|
|
100
101
|
});
|
|
101
102
|
|
|
103
|
+
it("marks non-exempt open tools for attention at the active threshold", () => {
|
|
104
|
+
const defaults = resolveControlConfig();
|
|
105
|
+
|
|
106
|
+
assert.equal(shouldEmitOpenToolAttention({ config: defaults, currentTool: "bash", currentToolStartedAt: 0, now: 239_999 }), false);
|
|
107
|
+
assert.equal(shouldEmitOpenToolAttention({ config: defaults, currentTool: "bash", currentToolStartedAt: 0, now: 240_000 }), true);
|
|
108
|
+
assert.equal(shouldEmitOpenToolAttention({ config: defaults, currentTool: "contact_supervisor", currentToolStartedAt: 0, now: 999_999 }), false);
|
|
109
|
+
assert.equal(shouldEmitOpenToolAttention({ config: { ...defaults, enabled: false }, currentTool: "bash", currentToolStartedAt: 0, now: 999_999 }), false);
|
|
110
|
+
});
|
|
111
|
+
|
|
102
112
|
it("supports opt-in turn and token long-running thresholds", () => {
|
|
103
113
|
const tokenBudget = resolveControlConfig(undefined, { activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 500_000 });
|
|
104
114
|
const turnBudget = resolveControlConfig(undefined, { activeNoticeAfterMs: 999_999, activeNoticeAfterTurns: 5 });
|
|
@@ -172,6 +182,24 @@ describe("subagent control attention state", () => {
|
|
|
172
182
|
assert.doesNotMatch(message, /Wait:/);
|
|
173
183
|
});
|
|
174
184
|
|
|
185
|
+
it("formats open-tool attention notices with tool facts", () => {
|
|
186
|
+
const event = buildControlEvent({
|
|
187
|
+
to: "needs_attention",
|
|
188
|
+
runId: "78f659a3",
|
|
189
|
+
agent: "worker",
|
|
190
|
+
reason: "tool_open_threshold",
|
|
191
|
+
message: "worker has had tool 'bash' open for 240s",
|
|
192
|
+
currentTool: "bash",
|
|
193
|
+
currentToolDurationMs: 240_000,
|
|
194
|
+
currentPath: "scripts/run-tests.sh",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const message = formatControlNoticeMessage(event, "subagent-worker-78f659a3");
|
|
198
|
+
|
|
199
|
+
assert.match(message, /worker has had tool 'bash' open for 240s/);
|
|
200
|
+
assert.match(message, /Facts: tool bash 240s \| path scripts\/run-tests\.sh/);
|
|
201
|
+
});
|
|
202
|
+
|
|
175
203
|
it("formats supervisor-request notices with pending-channel guidance", () => {
|
|
176
204
|
const event = buildControlEvent({
|
|
177
205
|
to: "needs_attention",
|
|
@@ -595,9 +595,13 @@ describe("subagent prompt runtime", () => {
|
|
|
595
595
|
|
|
596
596
|
it("registers child watchdog lifecycle handlers only when enabled by env", () => {
|
|
597
597
|
delete process.env[CHILD_WATCHDOG_CONFIG_ENV];
|
|
598
|
-
// Clear the ack capture env explicitly: when this test suite itself
|
|
599
|
-
// pi-subagents child, the runner sets
|
|
598
|
+
// Clear the ack capture and steer env vars explicitly: when this test suite itself
|
|
599
|
+
// runs inside a pi-subagents child, the runner sets them and extra agent_end
|
|
600
|
+
// handlers register (steering inbox is gated on SUBAGENT_STEER_INBOX_ENV).
|
|
600
601
|
delete process.env[RUNTIME_EXTENSION_ACK_PATH_ENV];
|
|
602
|
+
delete process.env[SUBAGENT_STEER_INBOX_ENV];
|
|
603
|
+
delete process.env[SUBAGENT_STEER_CAPABILITY_ENV];
|
|
604
|
+
delete process.env[SUBAGENT_STEER_ACK_DIR_ENV];
|
|
601
605
|
const handlersWithout = new Map<string, unknown[]>();
|
|
602
606
|
registerSubagentPromptRuntime({
|
|
603
607
|
on(event: string, handler: unknown) {
|
|
@@ -922,7 +926,7 @@ describe("subagent prompt runtime", () => {
|
|
|
922
926
|
assert.deepEqual(registered, ["subagent_wait"]);
|
|
923
927
|
});
|
|
924
928
|
|
|
925
|
-
it("
|
|
929
|
+
it("does not satisfy strict allowlists with native generic intercom", () => {
|
|
926
930
|
setSupervisorEnv();
|
|
927
931
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-intercom-diagnostic-"));
|
|
928
932
|
try {
|
|
@@ -944,9 +948,14 @@ describe("subagent prompt runtime", () => {
|
|
|
944
948
|
} as { on(event: string, handler: (payload?: unknown) => unknown): void; getAllTools(): Array<{ name: string }>; registerTool(tool: { name: string }): void });
|
|
945
949
|
|
|
946
950
|
handlers.get("session_start")?.({});
|
|
947
|
-
assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"
|
|
951
|
+
assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"]);
|
|
948
952
|
handlers.get("agent_start")?.({});
|
|
949
|
-
assert.
|
|
953
|
+
assert.deepEqual(readChildToolDiagnostic(diagnosticPath), {
|
|
954
|
+
agent: "scout",
|
|
955
|
+
required: ["read", "grep", "find", "ls", "bash", "edit", "write", "intercom"],
|
|
956
|
+
available: ["subagent_wait", "contact_supervisor"],
|
|
957
|
+
missing: ["intercom"],
|
|
958
|
+
});
|
|
950
959
|
} finally {
|
|
951
960
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
952
961
|
}
|
|
@@ -973,7 +982,7 @@ describe("subagent prompt runtime", () => {
|
|
|
973
982
|
assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"]);
|
|
974
983
|
});
|
|
975
984
|
|
|
976
|
-
it("registers native supervisor tools at runtime when pi-intercom is absent", async () => {
|
|
985
|
+
it("registers only native supervisor tools at runtime when pi-intercom is absent", async () => {
|
|
977
986
|
setSupervisorEnv();
|
|
978
987
|
const previousRequiredTools = process.env[REQUIRED_CHILD_TOOLS_ENV];
|
|
979
988
|
delete process.env[REQUIRED_CHILD_TOOLS_ENV];
|
|
@@ -995,7 +1004,7 @@ describe("subagent prompt runtime", () => {
|
|
|
995
1004
|
assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"]);
|
|
996
1005
|
|
|
997
1006
|
await handlers.get("before_agent_start")?.({ systemPrompt: BASE_PROMPT });
|
|
998
|
-
assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"
|
|
1007
|
+
assert.deepEqual(registered, ["subagent_wait", "contact_supervisor"]);
|
|
999
1008
|
} finally {
|
|
1000
1009
|
if (previousRequiredTools === undefined) delete process.env[REQUIRED_CHILD_TOOLS_ENV];
|
|
1001
1010
|
else process.env[REQUIRED_CHILD_TOOLS_ENV] = previousRequiredTools;
|
|
@@ -4,6 +4,7 @@ import type { Usage } from "../../src/shared/types.ts";
|
|
|
4
4
|
import {
|
|
5
5
|
MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS,
|
|
6
6
|
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
7
|
+
formatSubagentExtensionConflictError,
|
|
7
8
|
formatSubagentStartupRetryExhaustedError,
|
|
8
9
|
formatSubagentStartupRetryNote,
|
|
9
10
|
isRetryableSubagentStartupFailure,
|
|
@@ -69,6 +70,20 @@ describe("subagent startup retry", () => {
|
|
|
69
70
|
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ turnBudgetExceeded: true })), false);
|
|
70
71
|
});
|
|
71
72
|
|
|
73
|
+
it("adds actionable guidance for ambient extension registration conflicts", () => {
|
|
74
|
+
const conflict = 'Error: Failed to load extension "/tmp/pi-mcp-adapter-clone/index.ts": Tool "mcpScript" conflicts with /tmp/pi-mcp-adapter/index.ts\nError: Failed to load extension "/tmp/pi-mcp-adapter-clone/index.ts": Flag "--mcp-config" conflicts with /tmp/pi-mcp-adapter/index.ts';
|
|
75
|
+
const formatted = formatSubagentExtensionConflictError(conflict, {
|
|
76
|
+
agent: "reviewer",
|
|
77
|
+
ambientExtensionsEnabled: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.match(formatted ?? "", /loaded conflicting ambient Pi extensions/);
|
|
81
|
+
assert.match(formatted ?? "", /\.pi\/settings\.json/);
|
|
82
|
+
assert.match(formatted ?? "", /\{"subagents":\{"agentOverrides":\{"reviewer":\{"extensions":\[\]\}\}\}\}/);
|
|
83
|
+
assert.equal(formatSubagentExtensionConflictError(conflict, { agent: "reviewer", ambientExtensionsEnabled: false }), conflict);
|
|
84
|
+
assert.equal(formatSubagentExtensionConflictError("authentication failed", { agent: "reviewer", ambientExtensionsEnabled: true }), "authentication failed");
|
|
85
|
+
});
|
|
86
|
+
|
|
72
87
|
it("formats retry and exhaustion diagnostics without task content", () => {
|
|
73
88
|
assert.equal(
|
|
74
89
|
formatSubagentStartupRetryNote({ model: "openai/test", attempt: 1, maxAttempts: 4, delayMs: 250 }),
|
|
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import { updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
|
|
7
|
+
import { writeAsyncResultFile } from "../../src/runs/background/result-files.ts";
|
|
7
8
|
import { WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, waitForSubagents, type SubagentWaitDeps } from "../../src/runs/background/subagent-wait.ts";
|
|
8
9
|
import { recordWaitCompletion } from "../../src/runs/background/wait-completions.ts";
|
|
9
10
|
import type { AsyncStatus, SubagentState } from "../../src/shared/types.ts";
|
|
@@ -218,6 +219,32 @@ describe("subagent_wait tool", () => {
|
|
|
218
219
|
}
|
|
219
220
|
});
|
|
220
221
|
|
|
222
|
+
it("tells blocking callers to reply and wait for intercom-detached failed runs", async () => {
|
|
223
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-intercom-detach-"));
|
|
224
|
+
try {
|
|
225
|
+
const asyncRoot = path.join(root, "runs");
|
|
226
|
+
const sessionFile = path.join(root, "worker-session.jsonl");
|
|
227
|
+
fs.writeFileSync(sessionFile, "{}\n", "utf-8");
|
|
228
|
+
const state = makeState("sess-1");
|
|
229
|
+
writeStatus(asyncRoot, "run-detached", "running", { sessionId: "sess-1", pid: 999999 });
|
|
230
|
+
const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
|
|
231
|
+
sleep: async () => writeStatus(asyncRoot, "run-detached", "failed", {
|
|
232
|
+
sessionId: "sess-1",
|
|
233
|
+
error: "Step failed: worker",
|
|
234
|
+
steps: [{ agent: "worker", status: "failed", sessionFile, error: "Detached for intercom coordination before task completion." }],
|
|
235
|
+
}),
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
const text = textOf(result);
|
|
239
|
+
assert.match(text, /Reply to the supervisor request first/);
|
|
240
|
+
assert.match(text, /wait with subagent_wait/);
|
|
241
|
+
assert.match(text, /do not resume or launch a replacement/);
|
|
242
|
+
assert.doesNotMatch(text, /Resume-first/);
|
|
243
|
+
} finally {
|
|
244
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
221
248
|
it("surfaces terminal completion payloads from the result file in details.completions", async () => {
|
|
222
249
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-completions-file-"));
|
|
223
250
|
try {
|
|
@@ -270,6 +297,45 @@ describe("subagent_wait tool", () => {
|
|
|
270
297
|
}
|
|
271
298
|
});
|
|
272
299
|
|
|
300
|
+
it("surfaces terminal completion payloads from an indexed pending result file", async () => {
|
|
301
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-completions-pending-"));
|
|
302
|
+
const originalError = console.error;
|
|
303
|
+
try {
|
|
304
|
+
console.error = () => {};
|
|
305
|
+
const asyncRoot = path.join(root, "runs");
|
|
306
|
+
const resultsDir = path.join(root, "results");
|
|
307
|
+
const publicResultPath = path.join(resultsDir, "run-pending.json");
|
|
308
|
+
fs.mkdirSync(publicResultPath, { recursive: true });
|
|
309
|
+
const state = makeState("sess-1");
|
|
310
|
+
writeStatus(asyncRoot, "run-pending", "running", { sessionId: "sess-1", pid: 999999 });
|
|
311
|
+
|
|
312
|
+
const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
|
|
313
|
+
sleep: async () => {
|
|
314
|
+
writeStatus(asyncRoot, "run-pending", "complete", { sessionId: "sess-1" });
|
|
315
|
+
assert.deepEqual(writeAsyncResultFile(publicResultPath, {
|
|
316
|
+
id: "run-pending",
|
|
317
|
+
runId: "run-pending",
|
|
318
|
+
sessionId: "sess-1",
|
|
319
|
+
agent: "worker",
|
|
320
|
+
mode: "single",
|
|
321
|
+
state: "complete",
|
|
322
|
+
success: true,
|
|
323
|
+
results: [{ agent: "worker", success: true, outputState: "present" }],
|
|
324
|
+
}), { state: "pending" });
|
|
325
|
+
},
|
|
326
|
+
}));
|
|
327
|
+
|
|
328
|
+
assert.equal(result.isError, undefined);
|
|
329
|
+
assert.equal(result.details.completions?.[0]?.runId, "run-pending");
|
|
330
|
+
assert.equal(result.details.completions?.[0]?.success, true);
|
|
331
|
+
assert.equal(result.details.completions?.[0]?.results?.[0]?.agent, "worker");
|
|
332
|
+
assert.equal(fs.statSync(publicResultPath).isDirectory(), true);
|
|
333
|
+
} finally {
|
|
334
|
+
console.error = originalError;
|
|
335
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
273
339
|
it("reports malformed terminal result files as actionable errors", async () => {
|
|
274
340
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-completions-malformed-"));
|
|
275
341
|
try {
|
|
@@ -949,33 +1015,31 @@ describe("subagent_wait tool", () => {
|
|
|
949
1015
|
},
|
|
950
1016
|
};
|
|
951
1017
|
|
|
952
|
-
// A real timer-based sleep with a LONG poll interval; if wait waited for
|
|
953
|
-
// the poll it would take ~10s. The event should wake it in ~10ms.
|
|
954
1018
|
let sleepCalls = 0;
|
|
955
|
-
|
|
1019
|
+
let sleepArmed!: () => void;
|
|
1020
|
+
const armed = new Promise<void>((resolve) => { sleepArmed = resolve; });
|
|
1021
|
+
const sleep = (_ms: number, signal?: AbortSignal) => new Promise<void>((resolve) => {
|
|
956
1022
|
sleepCalls += 1;
|
|
957
|
-
|
|
958
|
-
|
|
1023
|
+
signal?.addEventListener("abort", resolve, { once: true });
|
|
1024
|
+
sleepArmed();
|
|
959
1025
|
});
|
|
960
1026
|
|
|
961
|
-
const startedAt = Date.now();
|
|
962
1027
|
const p = waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
|
|
963
1028
|
events,
|
|
964
1029
|
pollIntervalMs: 10_000,
|
|
965
|
-
sleep
|
|
1030
|
+
sleep,
|
|
966
1031
|
}));
|
|
967
1032
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
events.emit("subagent:async-complete", { id: "run-a" });
|
|
972
|
-
}, 15);
|
|
1033
|
+
await armed;
|
|
1034
|
+
writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
|
|
1035
|
+
events.emit("subagent:async-complete", { id: "run-a" });
|
|
973
1036
|
|
|
974
|
-
const result = await
|
|
975
|
-
|
|
1037
|
+
const result = await Promise.race([
|
|
1038
|
+
p,
|
|
1039
|
+
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error("event wake did not resolve subagent_wait")), 1_000)),
|
|
1040
|
+
]);
|
|
976
1041
|
assert.equal(result.isError, undefined);
|
|
977
1042
|
assert.match(textOf(result), /done/i);
|
|
978
|
-
assert.ok(elapsed < 5_000, `should wake via event, not the 10s poll; took ${elapsed}ms`);
|
|
979
1043
|
assert.ok(sleepCalls >= 1, "poll-interval sleep still armed as fallback");
|
|
980
1044
|
} finally {
|
|
981
1045
|
fs.rmSync(root, { recursive: true, force: true });
|
|
@@ -29,7 +29,7 @@ function parentToolEnv(agentDir?: string): NodeJS.ProcessEnv {
|
|
|
29
29
|
|
|
30
30
|
describe("registered subagent tool description", () => {
|
|
31
31
|
it("describes the four first-class execution modes", () => {
|
|
32
|
-
const description = buildSubagentToolDescription();
|
|
32
|
+
const description = buildSubagentToolDescription({ toolDescriptionMode: "full" });
|
|
33
33
|
assert.match(description, /^Delegate one child with \{ agent, task \}/i);
|
|
34
34
|
assert.match(description, /SINGLE: \{ agent, task\? \}/i);
|
|
35
35
|
assert.match(description, /CHAIN: \{ chain: \[\{ agent: "explorer"/i);
|
|
@@ -49,7 +49,7 @@ describe("registered subagent tool description", () => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it("includes legacy chain-control guidance when enabled", () => {
|
|
52
|
-
const description = buildSubagentToolDescription({ legacyChainControls: true });
|
|
52
|
+
const description = buildSubagentToolDescription({ toolDescriptionMode: "full", legacyChainControls: true });
|
|
53
53
|
assert.match(description, /append-step.*step:/i);
|
|
54
54
|
assert.match(description, /approve-checkpoint|reject-checkpoint/);
|
|
55
55
|
});
|
|
@@ -147,7 +147,7 @@ describe("registered subagent tool description", () => {
|
|
|
147
147
|
assert.doesNotMatch(description, /decide a paused durable legacy chain checkpoint/);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
it("falls back to
|
|
150
|
+
it("falls back to compact mode when custom mode has no valid file", () => {
|
|
151
151
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-missing-"));
|
|
152
152
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-agent-"));
|
|
153
153
|
const warnings: string[] = [];
|
|
@@ -157,11 +157,11 @@ describe("registered subagent tool description", () => {
|
|
|
157
157
|
{ cwd, agentDir, warn: (message) => warnings.push(message) },
|
|
158
158
|
);
|
|
159
159
|
|
|
160
|
-
assert.equal(description,
|
|
161
|
-
assert.ok(warnings.some((message) => message.includes("using
|
|
160
|
+
assert.equal(description, COMPACT_SUBAGENT_TOOL_DESCRIPTION);
|
|
161
|
+
assert.ok(warnings.some((message) => message.includes("using compact description")));
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
it("falls back to
|
|
164
|
+
it("falls back to compact mode when toolDescriptionMode is invalid", () => {
|
|
165
165
|
const warnings: string[] = [];
|
|
166
166
|
|
|
167
167
|
const description = buildSubagentToolDescription(
|
|
@@ -169,7 +169,7 @@ describe("registered subagent tool description", () => {
|
|
|
169
169
|
{ warn: (message) => warnings.push(message) },
|
|
170
170
|
);
|
|
171
171
|
|
|
172
|
-
assert.equal(description,
|
|
172
|
+
assert.equal(description, COMPACT_SUBAGENT_TOOL_DESCRIPTION);
|
|
173
173
|
assert.ok(warnings.some((message) => message.includes("Ignoring invalid toolDescriptionMode")));
|
|
174
174
|
});
|
|
175
175
|
|
|
@@ -219,7 +219,7 @@ describe("registered subagent tool description", () => {
|
|
|
219
219
|
|
|
220
220
|
it("registers full, compact, custom, and fallback descriptions from extension config", () => {
|
|
221
221
|
const defaultAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-default-"));
|
|
222
|
-
writeExtensionConfig(defaultAgentDir, { legacyChainControls: true });
|
|
222
|
+
writeExtensionConfig(defaultAgentDir, { toolDescriptionMode: "full", legacyChainControls: true });
|
|
223
223
|
assert.equal(readRegisteredTool(defaultAgentDir).description, FULL_SUBAGENT_TOOL_DESCRIPTION);
|
|
224
224
|
|
|
225
225
|
const compactAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-compact-"));
|
|
@@ -235,20 +235,20 @@ describe("registered subagent tool description", () => {
|
|
|
235
235
|
|
|
236
236
|
const missingCustomAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-missing-"));
|
|
237
237
|
writeExtensionConfig(missingCustomAgentDir, { toolDescriptionMode: "custom", legacyChainControls: true });
|
|
238
|
-
assert.equal(readRegisteredTool(missingCustomAgentDir).description,
|
|
238
|
+
assert.equal(readRegisteredTool(missingCustomAgentDir).description, COMPACT_SUBAGENT_TOOL_DESCRIPTION);
|
|
239
239
|
|
|
240
240
|
const invalidAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-invalid-"));
|
|
241
241
|
writeExtensionConfig(invalidAgentDir, { toolDescriptionMode: "tiny", legacyChainControls: true });
|
|
242
|
-
assert.equal(readRegisteredTool(invalidAgentDir).description,
|
|
242
|
+
assert.equal(readRegisteredTool(invalidAgentDir).description, COMPACT_SUBAGENT_TOOL_DESCRIPTION);
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
it("registers the full schema and
|
|
245
|
+
it("registers the full schema and chain description by default", () => {
|
|
246
246
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tool-desc-trimmed-"));
|
|
247
247
|
const tool = readRegisteredTool(agentDir);
|
|
248
248
|
assert.equal(tool.properties.includes("step"), true);
|
|
249
249
|
assert.equal(tool.properties.includes("chain"), true);
|
|
250
250
|
assert.equal(tool.properties.includes("tasks"), true);
|
|
251
|
-
assert.match(tool.description, /CHAIN
|
|
251
|
+
assert.match(tool.description, /CHAIN \{chain:/i);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
254
|
it("registers the trimmed schema and description when legacyChainControls is false", () => {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_FAST_TOOL_TIMEOUT_MS,
|
|
5
|
+
DEFAULT_FAST_TOOL_TIMEOUT_TOOLS,
|
|
6
|
+
TOOL_TIMEOUT_ALLOWLIST,
|
|
7
|
+
TOOL_TIMEOUT_ENV,
|
|
8
|
+
defaultToolTimeoutMs,
|
|
9
|
+
effectiveToolTimeoutMs,
|
|
10
|
+
formatToolTimeoutMessage,
|
|
11
|
+
isToolTimeoutExempt,
|
|
12
|
+
resolveToolTimeoutMs,
|
|
13
|
+
toolTimeoutCallKey,
|
|
14
|
+
toolTimeoutFromEnv,
|
|
15
|
+
} from "../../src/runs/shared/tool-timeout.ts";
|
|
16
|
+
|
|
17
|
+
describe("resolveToolTimeoutMs", () => {
|
|
18
|
+
it("has no configured hard timeout when nothing is configured anywhere", () => {
|
|
19
|
+
assert.deepEqual(resolveToolTimeoutMs({}), {});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("honors precedence: per-call > agent frontmatter > config > env", () => {
|
|
23
|
+
const input = {
|
|
24
|
+
callValue: 1_000,
|
|
25
|
+
agentValue: 2_000,
|
|
26
|
+
configValue: 3_000,
|
|
27
|
+
envValue: "4000",
|
|
28
|
+
};
|
|
29
|
+
assert.deepEqual(resolveToolTimeoutMs(input), { toolTimeoutMs: 1_000 });
|
|
30
|
+
assert.deepEqual(resolveToolTimeoutMs({ ...input, callValue: undefined }), { toolTimeoutMs: 2_000 });
|
|
31
|
+
assert.deepEqual(resolveToolTimeoutMs({ ...input, callValue: undefined, agentValue: undefined }), {
|
|
32
|
+
toolTimeoutMs: 3_000,
|
|
33
|
+
});
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
resolveToolTimeoutMs({ ...input, callValue: undefined, agentValue: undefined, configValue: undefined }),
|
|
36
|
+
{ toolTimeoutMs: 4_000 },
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accepts the env value as a plain integer string", () => {
|
|
41
|
+
assert.deepEqual(
|
|
42
|
+
resolveToolTimeoutMs({ envValue: "60000", callValue: undefined, agentValue: undefined, configValue: undefined }),
|
|
43
|
+
{ toolTimeoutMs: 60_000 },
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects non-positive, non-integer, and oversized values with an error", () => {
|
|
48
|
+
for (const bad of [0, -1, 1.5, "abc", Number.NaN]) {
|
|
49
|
+
const r = resolveToolTimeoutMs({ callValue: bad });
|
|
50
|
+
assert.ok(r.error, `expected error for ${String(bad)}`);
|
|
51
|
+
assert.match(r.error!, /positive integer/);
|
|
52
|
+
}
|
|
53
|
+
const huge = resolveToolTimeoutMs({ callValue: 2_147_483_648 });
|
|
54
|
+
assert.ok(huge.error);
|
|
55
|
+
assert.match(huge.error!, /no larger than/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("ignores an empty env string (treated as unset)", () => {
|
|
59
|
+
assert.deepEqual(resolveToolTimeoutMs({ envValue: " " }), {});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("effectiveToolTimeoutMs", () => {
|
|
64
|
+
it("uses a default hard timeout only for known-fast tools", () => {
|
|
65
|
+
for (const tool of ["read", "grep", "find", "ls", "edit", "write", "structured_output"]) {
|
|
66
|
+
assert.equal(defaultToolTimeoutMs(tool), DEFAULT_FAST_TOOL_TIMEOUT_MS, tool);
|
|
67
|
+
assert.equal(effectiveToolTimeoutMs(tool, undefined), DEFAULT_FAST_TOOL_TIMEOUT_MS, tool);
|
|
68
|
+
}
|
|
69
|
+
assert.equal(defaultToolTimeoutMs("bash"), undefined);
|
|
70
|
+
assert.equal(effectiveToolTimeoutMs("bash", undefined), undefined);
|
|
71
|
+
assert.deepEqual([...DEFAULT_FAST_TOOL_TIMEOUT_TOOLS].sort(), ["edit", "find", "grep", "ls", "read", "structured_output", "write"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("lets an explicit configured timeout win for non-exempt tools", () => {
|
|
75
|
+
assert.equal(effectiveToolTimeoutMs("bash", 1234), 1234);
|
|
76
|
+
assert.equal(effectiveToolTimeoutMs("read", 1234), 1234);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("never applies a tool timeout to blocking coordination tools", () => {
|
|
80
|
+
for (const tool of ["contact_supervisor", "intercom", "subagent_wait"]) {
|
|
81
|
+
assert.equal(isToolTimeoutExempt(tool), true, `${tool} must be exempt`);
|
|
82
|
+
assert.equal(effectiveToolTimeoutMs(tool, 1234), undefined);
|
|
83
|
+
assert.equal(effectiveToolTimeoutMs(tool, undefined), undefined);
|
|
84
|
+
}
|
|
85
|
+
assert.equal(isToolTimeoutExempt("bash"), false);
|
|
86
|
+
assert.equal(isToolTimeoutExempt(undefined), false);
|
|
87
|
+
assert.deepEqual([...TOOL_TIMEOUT_ALLOWLIST].sort(), ["contact_supervisor", "intercom", "subagent_wait"]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("tool timeout formatting and keys", () => {
|
|
92
|
+
it("formats timeout errors consistently", () => {
|
|
93
|
+
assert.equal(formatToolTimeoutMessage("bash", 1000), "Tool 'bash' exceeded its timeout of 1000ms.");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("uses toolCallId when present and falls back to an anonymous sequence", () => {
|
|
97
|
+
assert.equal(toolTimeoutCallKey({ toolCallId: "call-1", toolName: "read" }, 9), "id:call-1");
|
|
98
|
+
assert.equal(toolTimeoutCallKey({ toolName: "read" }, 9), "anon:read:9");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("toolTimeoutFromEnv", () => {
|
|
103
|
+
it("reads SELESAI_SUBAGENT_TOOL_TIMEOUT_MS from the process env, with PI_ fallback", () => {
|
|
104
|
+
assert.equal(TOOL_TIMEOUT_ENV, "SELESAI_SUBAGENT_TOOL_TIMEOUT_MS");
|
|
105
|
+
assert.equal(toolTimeoutFromEnv({ SELESAI_SUBAGENT_TOOL_TIMEOUT_MS: "123" }), "123");
|
|
106
|
+
assert.equal(toolTimeoutFromEnv({ PI_SUBAGENT_TOOL_TIMEOUT_MS: "456" }), "456");
|
|
107
|
+
assert.equal(toolTimeoutFromEnv({}), undefined);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -235,6 +235,40 @@ describe("non-blocking wait subscriptions", () => {
|
|
|
235
235
|
}
|
|
236
236
|
});
|
|
237
237
|
|
|
238
|
+
it("tells the parent to reply and wait for an intercom-detached failed async run", () => {
|
|
239
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-subscribe-intercom-detach-"));
|
|
240
|
+
const asyncRoot = path.join(root, "runs");
|
|
241
|
+
const subscriptionsDir = path.join(root, "subscriptions");
|
|
242
|
+
const sessionFile = path.join(root, "worker-session.jsonl");
|
|
243
|
+
const sent: string[] = [];
|
|
244
|
+
const state = makeState();
|
|
245
|
+
const manager = createWaitSubscriptionManager({
|
|
246
|
+
events: new TestBus(),
|
|
247
|
+
sendMessage(message: { content?: unknown }) { sent.push(String(message.content)); },
|
|
248
|
+
} as never, state, { asyncDirRoot: asyncRoot, subscriptionsDir, pollIntervalMs: 60_000, kill: () => true });
|
|
249
|
+
try {
|
|
250
|
+
fs.writeFileSync(sessionFile, "{}\n", "utf-8");
|
|
251
|
+
writeStatus(asyncRoot, "run-detached", "running", { sessionId: "session-a", pid: 999_999 });
|
|
252
|
+
manager.arm({ targetKind: "async", runId: "run-detached", requestedId: "run-detached", timeoutMs: 30_000 });
|
|
253
|
+
|
|
254
|
+
writeStatus(asyncRoot, "run-detached", "failed", {
|
|
255
|
+
sessionId: "session-a",
|
|
256
|
+
error: "Step failed: worker",
|
|
257
|
+
steps: [{ agent: "worker", status: "failed", sessionFile, error: "Detached for intercom coordination before task completion." }],
|
|
258
|
+
});
|
|
259
|
+
manager.reconcile();
|
|
260
|
+
|
|
261
|
+
const message = sent[0] ?? "";
|
|
262
|
+
assert.match(message, /Reply to the supervisor request first/);
|
|
263
|
+
assert.match(message, /wait with subagent_wait/);
|
|
264
|
+
assert.match(message, /do not resume or launch a replacement/);
|
|
265
|
+
assert.doesNotMatch(message, /Resume-first/);
|
|
266
|
+
} finally {
|
|
267
|
+
manager.dispose();
|
|
268
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
238
272
|
it("waits for foreground run restoration before reconciling a restored subscription", () => {
|
|
239
273
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-subscribe-foreground-restore-"));
|
|
240
274
|
const subscriptionsDir = path.join(root, "subscriptions");
|
|
@@ -4,7 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { describe, it } from "node:test";
|
|
7
|
-
import { isSameGitRepository, resolveWorkflowChatProgress } from "../../src/workflows/chat-progress.ts";
|
|
7
|
+
import { buildWorkflowChatProgressRows, isSameGitRepository, resolveWorkflowChatProgress } from "../../src/workflows/chat-progress.ts";
|
|
8
8
|
import { renderSubagentResult } from "../../src/tui/render.ts";
|
|
9
9
|
import { bindMissionWorkflowChildAsyncLaunch, createSubagentExecutor, foregroundResultIntercomStatus, missionWorkflowChildStatus, runMissionWorkflowChild, shouldSuppressRoutineResultIntercom } from "../../src/runs/foreground/subagent-executor.ts";
|
|
10
10
|
import { readMissionBinding } from "../../src/missions/lifecycle.ts";
|
|
@@ -202,6 +202,38 @@ describe("workflow chat progress rendering", () => {
|
|
|
202
202
|
}
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
it("warns once when a workflow child outlives its mission record", async () => {
|
|
206
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-workflow-missing-mission-"));
|
|
207
|
+
const originalWarn = console.warn;
|
|
208
|
+
const warnings: string[] = [];
|
|
209
|
+
try {
|
|
210
|
+
const location = {
|
|
211
|
+
projectRoot: root,
|
|
212
|
+
missionDir: path.join(root, ".pi/subagents", "missions"),
|
|
213
|
+
globalIndexDir: path.join(root, ".pi/subagents", "mission-index"),
|
|
214
|
+
writeGlobalIndex: false,
|
|
215
|
+
};
|
|
216
|
+
const mission = createMission(location, { title: "Workflow", objective: "Track child" });
|
|
217
|
+
fs.rmSync(path.join(location.missionDir, `${mission.id}.json`));
|
|
218
|
+
console.warn = (...args: unknown[]) => warnings.push(args.map(String).join(" "));
|
|
219
|
+
|
|
220
|
+
for (const key of ["first", "second"]) {
|
|
221
|
+
await assert.rejects(
|
|
222
|
+
() => runMissionWorkflowChild({ missionId: mission.id, location, autoCreated: false }, "wf-missing-mission", key, "Prep", async () => {
|
|
223
|
+
throw new Error("child failed");
|
|
224
|
+
}),
|
|
225
|
+
/child failed/,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
assert.equal(warnings.length, 1);
|
|
230
|
+
assert.match(warnings[0] ?? "", new RegExp(`Mission '${mission.id}' is no longer in the mission store`));
|
|
231
|
+
} finally {
|
|
232
|
+
console.warn = originalWarn;
|
|
233
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
205
237
|
it("renders stable rows keyed by workflow trace entries", () => {
|
|
206
238
|
const text = componentText(renderSubagentResult({
|
|
207
239
|
content: [{ type: "text", text: "Workflow running." }],
|
|
@@ -215,6 +247,7 @@ describe("workflow chat progress rendering", () => {
|
|
|
215
247
|
{ operation: "run", key: "scout", state: "completed", runId: "run-scout", phase: "Validation", label: "Found renderer seam", durationMs: 12 },
|
|
216
248
|
{ operation: "run", key: "tests", state: "started", phase: "Validation", label: "focused integration suite" },
|
|
217
249
|
{ operation: "run", key: "review", state: "failed", phase: "Validation", label: "fresh-context UX review", error: "needs fixes" },
|
|
250
|
+
{ operation: "run", key: "stale", state: "stopped", phase: "Validation", label: "superseded exact-head review", error: "Workflow stopped by user." },
|
|
218
251
|
],
|
|
219
252
|
emits: [],
|
|
220
253
|
console: [],
|
|
@@ -228,6 +261,66 @@ describe("workflow chat progress rendering", () => {
|
|
|
228
261
|
assert.match(text, /complete\s+scout Found renderer seam/);
|
|
229
262
|
assert.match(text, /running\s+tests focused integration suite/);
|
|
230
263
|
assert.match(text, /failed\s+review fresh-context UX review .* needs fixes/);
|
|
264
|
+
assert.match(text, /stopped\s+stale superseded exact-head review .* Workflow stopped by user/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("renders detached workflow trace rows as paused attention", () => {
|
|
268
|
+
const trace: NonNullable<Details["workflow"]>["trace"] = [{
|
|
269
|
+
operation: "run",
|
|
270
|
+
key: "detaches",
|
|
271
|
+
state: "detached",
|
|
272
|
+
phase: "Decision",
|
|
273
|
+
label: "supervisor handoff",
|
|
274
|
+
error: "Detached for intercom coordination. Reply to the supervisor request first.",
|
|
275
|
+
}];
|
|
276
|
+
const rows = buildWorkflowChatProgressRows(trace);
|
|
277
|
+
assert.equal(rows[0]?.state, "detached");
|
|
278
|
+
|
|
279
|
+
const text = componentText(renderSubagentResult({
|
|
280
|
+
content: [{ type: "text", text: "Workflow paused." }],
|
|
281
|
+
details: {
|
|
282
|
+
mode: "workflow",
|
|
283
|
+
runId: "wf_detached",
|
|
284
|
+
results: [],
|
|
285
|
+
chatProgress: { mode: "live-card", repoRelation: "same", repoLabel: "pi-subagents" },
|
|
286
|
+
workflow: { trace, emits: [], console: [] },
|
|
287
|
+
},
|
|
288
|
+
}, { expanded: false }, theme as any));
|
|
289
|
+
|
|
290
|
+
assert.match(text, /workflow wf_detached .* same repo .* paused/);
|
|
291
|
+
assert.match(text, /Phase Decision/);
|
|
292
|
+
assert.match(text, /detached\s+detaches supervisor handoff .* Detached for intercom coordination/);
|
|
293
|
+
assert.doesNotMatch(text, /running\s+detaches/);
|
|
294
|
+
assert.doesNotMatch(text, /failed\s+detaches/);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("applies main-window density settings to collapsed workflow live cards", () => {
|
|
298
|
+
const trace = Array.from({ length: 10 }, (_, index) => ({
|
|
299
|
+
operation: "run" as const,
|
|
300
|
+
key: `step-${index}`,
|
|
301
|
+
state: "started" as const,
|
|
302
|
+
label: `review ${index}`,
|
|
303
|
+
}));
|
|
304
|
+
const result = {
|
|
305
|
+
content: [{ type: "text" as const, text: "Workflow running." }],
|
|
306
|
+
details: {
|
|
307
|
+
mode: "workflow" as const,
|
|
308
|
+
runId: "wf_density",
|
|
309
|
+
results: [],
|
|
310
|
+
chatProgress: { mode: "live-card" as const, repoRelation: "same" as const, repoLabel: "pi-subagents" },
|
|
311
|
+
workflow: { trace, emits: [], console: [] },
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const compact = renderSubagentResult(result, { expanded: false }, theme as any, undefined, { horizontalSpacing: 0, compactResultMaxLines: 3 }).render(120);
|
|
316
|
+
assert.equal(compact.length, 3);
|
|
317
|
+
assert.match(compact[1]!, /^Repo pi-subagents\s*$/);
|
|
318
|
+
assert.match(compact[2]!, /rows hidden/);
|
|
319
|
+
|
|
320
|
+
const expanded = renderSubagentResult(result, { expanded: true }, theme as any, undefined, { horizontalSpacing: 0, compactResultMaxLines: 3 }).render(120);
|
|
321
|
+
assert.ok(expanded.length > 3);
|
|
322
|
+
assert.match(expanded[1]!, /^ Repo pi-subagents\s*$/);
|
|
323
|
+
assert.doesNotMatch(expanded.join("\n"), /rows hidden · .* expands/);
|
|
231
324
|
});
|
|
232
325
|
|
|
233
326
|
it("bounds workflow live-card rows and keeps old failed children visible", () => {
|