@selesai/code 0.5.27 → 0.5.29
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/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +9 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +1 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/credential-print.d.ts +23 -0
- package/dist/cli/credential-print.d.ts.map +1 -0
- package/dist/cli/credential-print.js +117 -0
- package/dist/cli/credential-print.js.map +1 -0
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +1 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +3 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +12 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +20 -14
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +11 -3
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +11 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +14 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +10 -0
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/llama/provider.d.ts.map +1 -1
- package/dist/core/llama/provider.js +8 -3
- package/dist/core/llama/provider.js.map +1 -1
- package/dist/core/model-config.d.ts +30 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +6 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +2 -2
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +20 -3
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-runtime.d.ts +2 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +5 -4
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +13 -6
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +24 -11
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts +15 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +66 -9
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +2 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +34 -5
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +3 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/extensions/pi-intercom/CHANGELOG.md +249 -0
- package/dist/extensions/pi-intercom/README.md +102 -39
- package/dist/extensions/pi-intercom/broker/broker.ts +1233 -36
- package/dist/extensions/pi-intercom/broker/client.test.ts +83 -0
- package/dist/extensions/pi-intercom/broker/client.ts +315 -12
- package/dist/extensions/pi-intercom/broker/extension-state.ts +186 -0
- package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -0
- package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -0
- package/dist/extensions/pi-intercom/broker/framing.ts +82 -24
- package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -0
- package/dist/extensions/pi-intercom/broker/paths.ts +117 -8
- package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -0
- package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -0
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +160 -23
- package/dist/extensions/pi-intercom/broker/spawn.ts +113 -27
- package/dist/extensions/pi-intercom/config.test.ts +93 -0
- package/dist/extensions/pi-intercom/config.ts +55 -6
- package/dist/extensions/pi-intercom/cwd.test.ts +40 -0
- package/dist/extensions/pi-intercom/cwd.ts +31 -0
- package/dist/extensions/pi-intercom/extension-api.ts +44 -0
- package/dist/extensions/pi-intercom/format-context.test.ts +31 -0
- package/dist/extensions/pi-intercom/format-context.ts +32 -0
- package/dist/extensions/pi-intercom/index.ts +742 -145
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +2646 -0
- package/dist/extensions/pi-intercom/package.json +15 -5
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +134 -0
- package/dist/extensions/pi-intercom/reply-tracker.ts +31 -13
- package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +13 -11
- package/dist/extensions/pi-intercom/test/inline-message.test.ts +184 -0
- package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -0
- package/dist/extensions/pi-intercom/types.ts +94 -4
- package/dist/extensions/pi-intercom/ui/compose.ts +8 -4
- package/dist/extensions/pi-intercom/ui/inline-message.ts +61 -25
- package/dist/extensions/pi-intercom/ui/session-list.ts +7 -3
- package/dist/extensions/pi-subagents/CHANGELOG.md +85 -0
- package/dist/extensions/pi-subagents/LICENSE +21 -0
- package/dist/extensions/pi-subagents/README.md +146 -52
- package/dist/extensions/pi-subagents/agents/architect.md +1 -0
- package/dist/extensions/pi-subagents/agents/builder.md +1 -0
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +3 -3
- package/dist/extensions/pi-subagents/prompts/review-loop.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +21 -989
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +430 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +282 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +71 -30
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +4 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +118 -9
- package/dist/extensions/pi-subagents/src/agents/skills.ts +14 -12
- package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +16 -12
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +17 -7
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -6
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +8 -7
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +9 -4
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +33 -4
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +63 -16
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +24 -13
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +48 -5
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +68 -1
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +29 -4
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +29 -6
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +372 -28
- package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +65 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +143 -43
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +527 -247
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +426 -123
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +15 -7
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +51 -19
- package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +8 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +97 -20
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +43 -12
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +52 -4
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +19 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +45 -9
- package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +31 -1
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +101 -0
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/usage-budget.ts +65 -0
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +26 -1
- package/dist/extensions/pi-subagents/src/shared/settings.ts +17 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +155 -10
- package/dist/extensions/pi-subagents/src/shared/utils.ts +73 -4
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +46 -4
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +5 -3
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +87 -24
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +182 -9
- package/dist/extensions/pi-subagents/src/tui/render.ts +27 -28
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +14 -7
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +5 -4
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +170 -17
- package/dist/extensions/pi-subagents/src/watchdog/scope.ts +62 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +41 -1
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +10 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +348 -7
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +4 -2
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +51 -2
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +213 -13
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +118 -12
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +850 -7
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +112 -23
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -5
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +3 -3
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +47 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +60 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +102 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +6 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +48 -48
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +4 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +176 -8
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +164 -5
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +11 -11
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +18 -2
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +20 -1
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +30 -2
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +10 -2
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +46 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +13 -1
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +29 -4
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +223 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +30 -0
- package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +34 -2
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +41 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +3 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +210 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +1 -1
- package/dist/extensions/question/index.ts +14 -1
- package/dist/extensions/question/tests/wizard.test.ts +11 -8
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +43 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +3 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +10 -2
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +16 -46
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +4 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts +12 -0
- package/dist/modes/interactive/external-editor.d.ts.map +1 -0
- package/dist/modes/interactive/external-editor.js +37 -0
- package/dist/modes/interactive/external-editor.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +72 -64
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +14 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/skills/batch-grill-me/SKILL.md +3 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +19 -8
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +1 -1
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/custom-provider.md +14 -5
- package/docs/environment-variables.md +86 -0
- package/docs/extensions.md +15 -5
- package/docs/index.md +1 -0
- package/docs/models.md +9 -2
- package/docs/providers.md +21 -2
- package/docs/rpc.md +23 -6
- package/docs/session-format.md +2 -0
- package/docs/settings.md +1 -1
- package/docs/usage.md +0 -13
- package/examples/extensions/custom-compaction.ts +5 -2
- package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/handoff.ts +11 -3
- package/examples/extensions/message-renderer.ts +3 -3
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/summarize.ts +5 -2
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/12-full-control.ts +3 -1
- package/package.json +15 -6
- package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
|
@@ -42,12 +42,12 @@ function once(events: FakeEvents, event: string): Promise<unknown> {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function ctx() {
|
|
45
|
+
function ctx(sessionId = "session-123", sessionFile = "/sessions/parent.jsonl") {
|
|
46
46
|
return {
|
|
47
47
|
cwd: "/repo",
|
|
48
48
|
sessionManager: {
|
|
49
|
-
getSessionId: () =>
|
|
50
|
-
getSessionFile: () =>
|
|
49
|
+
getSessionId: () => sessionId,
|
|
50
|
+
getSessionFile: () => sessionFile,
|
|
51
51
|
},
|
|
52
52
|
} as any;
|
|
53
53
|
}
|
|
@@ -91,6 +91,14 @@ describe("subagent extension RPC bridge", () => {
|
|
|
91
91
|
(reply as { data: { capabilities?: { nonRecoveringSteer?: boolean } } }).data.capabilities?.nonRecoveringSteer,
|
|
92
92
|
true,
|
|
93
93
|
);
|
|
94
|
+
assert.equal(
|
|
95
|
+
(reply as { data: { capabilities?: { resume?: boolean } } }).data.capabilities?.resume,
|
|
96
|
+
true,
|
|
97
|
+
);
|
|
98
|
+
assert.deepEqual(
|
|
99
|
+
(reply as { data: { capabilities?: { fleetStatus?: unknown } } }).data.capabilities?.fleetStatus,
|
|
100
|
+
{ version: 1 },
|
|
101
|
+
);
|
|
94
102
|
|
|
95
103
|
bridge.dispose();
|
|
96
104
|
});
|
|
@@ -137,7 +145,149 @@ describe("subagent extension RPC bridge", () => {
|
|
|
137
145
|
assert.equal(reply.success, true);
|
|
138
146
|
assert.deepEqual(executedParams, { action: "status", id: "abc123" });
|
|
139
147
|
assert.equal((reply as { data: { text?: string } }).data.text, "Run: abc123");
|
|
148
|
+
assert.deepEqual((reply as { data: { fleet?: unknown } }).data.fleet, {
|
|
149
|
+
version: 1, entries: [], totalActive: 0, omitted: 0,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
bridge.dispose();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("projects bounded display-safe active fleet records without internal ids", async () => {
|
|
156
|
+
const events = new FakeEvents();
|
|
157
|
+
const state = {
|
|
158
|
+
currentSessionId: "/sessions/parent.jsonl",
|
|
159
|
+
foregroundControls: new Map(),
|
|
160
|
+
asyncJobs: new Map([["async-private-id", {
|
|
161
|
+
asyncId: "async-private-id", sessionId: "/sessions/parent.jsonl", status: "running", mode: "single",
|
|
162
|
+
description: ["Review", "\u001b]8;;hostile\u0007", "the diff"].join("\n"),
|
|
163
|
+
startedAt: 100, steps: [{ agent: "reviewer", label: "opaque label", status: "running", startedAt: 120, model: "anthropic/claude-opus-4-8:high", thinking: "high", tokens: { input: 12, output: 34, total: 46 } }],
|
|
164
|
+
}]]),
|
|
165
|
+
} as any;
|
|
166
|
+
const bridge = registerSubagentRpcBridge({
|
|
167
|
+
events, getContext: () => ctx("runtime-session-id", "/sessions/parent.jsonl"), state,
|
|
168
|
+
execute: async () => ({ content: [{ type: "text", text: "Active async runs: 1" }], details: { mode: "management", results: [] } } as any),
|
|
169
|
+
});
|
|
170
|
+
const reply = await request(events, "fleet-status", "status");
|
|
171
|
+
const fleet = (reply as { data: { fleet: { entries: Array<Record<string, unknown>> } } }).data.fleet;
|
|
172
|
+
assert.equal(fleet.entries.length, 1);
|
|
173
|
+
assert.equal((fleet as { totalActive?: number }).totalActive, 1);
|
|
174
|
+
assert.equal((fleet as { omitted?: number }).omitted, 0);
|
|
175
|
+
assert.deepEqual(fleet.entries[0], {
|
|
176
|
+
key: "fleet-1", agent: "reviewer", role: "opaque label", model: "anthropic/claude-opus-4-8:high", effort: "high",
|
|
177
|
+
startedAt: 120, tokens: { input: 12, output: 34, total: 46 }, goal: "Review the diff",
|
|
178
|
+
});
|
|
179
|
+
assert.equal(JSON.stringify(fleet).includes("async-private-id"), false);
|
|
180
|
+
bridge.dispose();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("projects resolved foreground model, effort, split usage, and goal", async () => {
|
|
184
|
+
const events = new FakeEvents();
|
|
185
|
+
const state = {
|
|
186
|
+
currentSessionId: "session-123",
|
|
187
|
+
foregroundControls: new Map([["private-run", {
|
|
188
|
+
runId: "private-run",
|
|
189
|
+
sessionId: "session-123",
|
|
190
|
+
mode: "single",
|
|
191
|
+
startedAt: 90,
|
|
192
|
+
activeChildren: new Map([[0, {
|
|
193
|
+
index: 0,
|
|
194
|
+
agent: "worker",
|
|
195
|
+
description: "Implement the fix",
|
|
196
|
+
startedAt: 100,
|
|
197
|
+
updatedAt: 110,
|
|
198
|
+
model: "openai/gpt-5.6-terra:high",
|
|
199
|
+
thinking: "high",
|
|
200
|
+
inputTokens: 321,
|
|
201
|
+
outputTokens: 45,
|
|
202
|
+
tokens: 366,
|
|
203
|
+
}]]),
|
|
204
|
+
}]]),
|
|
205
|
+
asyncJobs: new Map(),
|
|
206
|
+
} as any;
|
|
207
|
+
state.foregroundControls.set("private-old", {
|
|
208
|
+
runId: "private-old",
|
|
209
|
+
sessionId: "old-session",
|
|
210
|
+
mode: "single",
|
|
211
|
+
startedAt: 50,
|
|
212
|
+
currentAgent: "reviewer",
|
|
213
|
+
description: "Old work",
|
|
214
|
+
});
|
|
215
|
+
const bridge = registerSubagentRpcBridge({
|
|
216
|
+
events,
|
|
217
|
+
getContext: () => ctx("session-123", "session-123"),
|
|
218
|
+
state,
|
|
219
|
+
execute: async () => ({ content: [], details: { mode: "management", results: [] } } as any),
|
|
220
|
+
});
|
|
221
|
+
const reply = await request(events, "foreground-fleet", "status");
|
|
222
|
+
assert.deepEqual((reply as any).data.fleet, {
|
|
223
|
+
version: 1,
|
|
224
|
+
totalActive: 1,
|
|
225
|
+
omitted: 0,
|
|
226
|
+
entries: [{
|
|
227
|
+
key: "fleet-1",
|
|
228
|
+
agent: "worker",
|
|
229
|
+
model: "openai/gpt-5.6-terra:high",
|
|
230
|
+
effort: "high",
|
|
231
|
+
startedAt: 100,
|
|
232
|
+
tokens: { input: 321, output: 45, total: 366 },
|
|
233
|
+
goal: "Implement the fix",
|
|
234
|
+
}],
|
|
235
|
+
});
|
|
236
|
+
assert.equal(JSON.stringify((reply as any).data.fleet).includes("private-run"), false);
|
|
237
|
+
bridge.dispose();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("uses monotonic opaque keys across removal/insertion and resets them per session", async () => {
|
|
241
|
+
const events = new FakeEvents();
|
|
242
|
+
const jobs = new Map<string, any>([
|
|
243
|
+
["private-a", { asyncId: "private-a", sessionId: "A", status: "running", mode: "single", startedAt: 1, agents: ["alpha"] }],
|
|
244
|
+
["private-b", { asyncId: "private-b", sessionId: "A", status: "running", mode: "single", startedAt: 2, agents: ["beta"] }],
|
|
245
|
+
["private-unattributed", { asyncId: "private-unattributed", status: "running", mode: "single", startedAt: 3, agents: ["unknown"] }],
|
|
246
|
+
]);
|
|
247
|
+
const state = { currentSessionId: "A", foregroundControls: new Map(), asyncJobs: jobs } as any;
|
|
248
|
+
let activeSession = "A";
|
|
249
|
+
const bridge = registerSubagentRpcBridge({ events, getContext: () => ctx(activeSession, activeSession), state, execute: async () => ({ content: [], details: { mode: "management", results: [] } } as any) });
|
|
250
|
+
const keys = async (id: string) => ((await request(events, id, "status")) as any).data.fleet.entries.map((entry: { key: string }) => entry.key);
|
|
251
|
+
assert.deepEqual(await keys("keys-a"), ["fleet-1", "fleet-2"]);
|
|
252
|
+
jobs.delete("private-b"); jobs.set("private-c", { asyncId: "private-c", sessionId: "A", status: "running", mode: "single", startedAt: 3, agents: ["gamma"] });
|
|
253
|
+
assert.deepEqual(await keys("keys-b"), ["fleet-1", "fleet-3"]);
|
|
254
|
+
state.currentSessionId = "B";
|
|
255
|
+
activeSession = "B";
|
|
256
|
+
assert.deepEqual(await keys("keys-c"), []);
|
|
257
|
+
jobs.set("private-d", { asyncId: "private-d", sessionId: "B", status: "running", mode: "single", startedAt: 4, agents: ["delta"] });
|
|
258
|
+
assert.deepEqual(await keys("keys-d"), ["fleet-1"]);
|
|
259
|
+
bridge.dispose();
|
|
260
|
+
});
|
|
140
261
|
|
|
262
|
+
it("reports bounded overflow and excludes unattributed or foreign-session jobs", async () => {
|
|
263
|
+
const events = new FakeEvents();
|
|
264
|
+
const jobs = new Map<string, any>();
|
|
265
|
+
for (let index = 0; index < 18; index += 1) {
|
|
266
|
+
jobs.set(`private-${index}`, {
|
|
267
|
+
asyncId: `private-${index}`,
|
|
268
|
+
sessionId: "session-123",
|
|
269
|
+
status: "running",
|
|
270
|
+
mode: "single",
|
|
271
|
+
startedAt: index + 1,
|
|
272
|
+
agents: [`worker-${index}`],
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
jobs.set("unattributed", { asyncId: "unattributed", status: "running", mode: "single", startedAt: 20, agents: ["hidden"] });
|
|
276
|
+
jobs.set("foreign", { asyncId: "foreign", sessionId: "other", status: "running", mode: "single", startedAt: 21, agents: ["hidden"] });
|
|
277
|
+
const state = { currentSessionId: "session-123", foregroundControls: new Map(), asyncJobs: jobs } as any;
|
|
278
|
+
const bridge = registerSubagentRpcBridge({
|
|
279
|
+
events,
|
|
280
|
+
getContext: () => ctx("session-123", "session-123"),
|
|
281
|
+
state,
|
|
282
|
+
execute: async () => ({ content: [], details: { mode: "management", results: [] } } as any),
|
|
283
|
+
});
|
|
284
|
+
const reply = await request(events, "fleet-overflow", "status");
|
|
285
|
+
const fleet = (reply as any).data.fleet;
|
|
286
|
+
assert.equal(fleet.entries.length, 16);
|
|
287
|
+
assert.equal(fleet.totalActive, 18);
|
|
288
|
+
assert.equal(fleet.omitted, 2);
|
|
289
|
+
assert.equal(JSON.stringify(fleet).includes("unattributed"), false);
|
|
290
|
+
assert.equal(JSON.stringify(fleet).includes("foreign"), false);
|
|
141
291
|
bridge.dispose();
|
|
142
292
|
});
|
|
143
293
|
|
|
@@ -298,6 +448,75 @@ describe("subagent extension RPC bridge", () => {
|
|
|
298
448
|
bridge.dispose();
|
|
299
449
|
});
|
|
300
450
|
|
|
451
|
+
it("delegates resume through the existing package-owned revival action", async () => {
|
|
452
|
+
const events = new FakeEvents();
|
|
453
|
+
let executedParams: unknown;
|
|
454
|
+
const bridge = registerSubagentRpcBridge({
|
|
455
|
+
events,
|
|
456
|
+
getContext: () => ctx(),
|
|
457
|
+
execute: async (_id, params) => {
|
|
458
|
+
executedParams = params;
|
|
459
|
+
return {
|
|
460
|
+
content: [{ type: "text", text: "Revived async subagent from run-1." }],
|
|
461
|
+
details: { mode: "single", results: [], asyncId: "run-2", asyncDir: "/tmp/run-2" },
|
|
462
|
+
} as any;
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const reply = await request(events, "resume-1", "resume", {
|
|
467
|
+
id: "run-1",
|
|
468
|
+
index: 0,
|
|
469
|
+
message: " Continue with the focused review. ",
|
|
470
|
+
output: " /tmp/revived-output.md ",
|
|
471
|
+
outputMode: "file-only",
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
assert.equal(reply.success, true);
|
|
475
|
+
assert.deepEqual(executedParams, {
|
|
476
|
+
action: "resume",
|
|
477
|
+
id: "run-1",
|
|
478
|
+
index: 0,
|
|
479
|
+
message: "Continue with the focused review.",
|
|
480
|
+
output: "/tmp/revived-output.md",
|
|
481
|
+
outputMode: "file-only",
|
|
482
|
+
});
|
|
483
|
+
assert.equal((reply as { data: { details?: { asyncId?: string } } }).data.details?.asyncId, "run-2");
|
|
484
|
+
|
|
485
|
+
bridge.dispose();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("rejects targetless or empty RPC resume before executor dispatch", async () => {
|
|
489
|
+
const events = new FakeEvents();
|
|
490
|
+
let executeCalls = 0;
|
|
491
|
+
const bridge = registerSubagentRpcBridge({
|
|
492
|
+
events,
|
|
493
|
+
getContext: () => ctx(),
|
|
494
|
+
execute: async () => {
|
|
495
|
+
executeCalls++;
|
|
496
|
+
return { content: [], details: { mode: "management", results: [] } } as any;
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const targetless = await request(events, "resume-no-target", "resume", { message: "continue" });
|
|
501
|
+
const empty = await request(events, "resume-empty", "resume", { id: "run-1", message: " " });
|
|
502
|
+
const inlineOutput = await request(events, "resume-inline", "resume", {
|
|
503
|
+
id: "run-1",
|
|
504
|
+
message: "continue",
|
|
505
|
+
output: "/tmp/output.md",
|
|
506
|
+
outputMode: "inline",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
assert.equal(targetless.success, false);
|
|
510
|
+
assert.equal((targetless as { error: { code: string } }).error.code, "invalid_params");
|
|
511
|
+
assert.match((targetless as { error: { message: string } }).error.message, /requires id, runId, or dir/);
|
|
512
|
+
assert.equal(empty.success, false);
|
|
513
|
+
assert.equal((empty as { error: { code: string } }).error.code, "invalid_params");
|
|
514
|
+
assert.equal(inlineOutput.success, false);
|
|
515
|
+
assert.match((inlineOutput as { error: { message: string } }).error.message, /file-only/);
|
|
516
|
+
assert.equal(executeCalls, 0);
|
|
517
|
+
bridge.dispose();
|
|
518
|
+
});
|
|
519
|
+
|
|
301
520
|
it("delegates interrupt through the existing executor action", async () => {
|
|
302
521
|
const events = new FakeEvents();
|
|
303
522
|
let executedParams: unknown;
|
|
@@ -328,7 +547,7 @@ describe("subagent extension RPC bridge", () => {
|
|
|
328
547
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
329
548
|
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
|
|
330
549
|
runId: "run-stop",
|
|
331
|
-
sessionId: "
|
|
550
|
+
sessionId: "/sessions/parent.jsonl",
|
|
332
551
|
mode: "single",
|
|
333
552
|
state: "running",
|
|
334
553
|
pid: 4242,
|
|
@@ -994,6 +994,36 @@ describe("async run status inspection", () => {
|
|
|
994
994
|
}
|
|
995
995
|
});
|
|
996
996
|
|
|
997
|
+
it("shows signal-terminated result-only runs as stopped", () => {
|
|
998
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-signal-result-"));
|
|
999
|
+
try {
|
|
1000
|
+
const asyncRoot = path.join(root, "runs");
|
|
1001
|
+
const resultsDir = path.join(root, "results");
|
|
1002
|
+
fs.mkdirSync(path.join(asyncRoot, "run-signal-result"), { recursive: true });
|
|
1003
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
1004
|
+
fs.writeFileSync(path.join(resultsDir, "run-signal-result.json"), JSON.stringify({
|
|
1005
|
+
id: "run-signal-result",
|
|
1006
|
+
agent: "worker",
|
|
1007
|
+
success: false,
|
|
1008
|
+
state: "failed",
|
|
1009
|
+
summary: "Subagent process terminated by signal SIGTERM.",
|
|
1010
|
+
results: [{ agent: "worker", success: false, exitCode: 1, processSignal: "SIGTERM" }],
|
|
1011
|
+
}, null, 2), "utf-8");
|
|
1012
|
+
|
|
1013
|
+
const result = inspectSubagentStatus({ id: "run-signal-result" }, {
|
|
1014
|
+
asyncDirRoot: asyncRoot,
|
|
1015
|
+
resultsDir,
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
const text = textContent(result);
|
|
1019
|
+
assert.equal(result.isError, undefined);
|
|
1020
|
+
assert.match(text, /State: stopped/);
|
|
1021
|
+
assert.match(text, /Resume: unavailable; stopped runs are not resumable/);
|
|
1022
|
+
} finally {
|
|
1023
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
|
|
997
1027
|
it("falls back to an existing result when async dir has no status file", () => {
|
|
998
1028
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-result-fallback-"));
|
|
999
1029
|
try {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { MAX_RUNTIME_ACKNOWLEDGED_EXTENSION_FILE_BYTES, isRuntimeAcknowledgedExtensionId, projectRuntimeAcknowledgedExtensions, readRuntimeAcknowledgedExtensions, sanitizeRuntimeAcknowledgedExtensions } from "../../src/runs/shared/runtime-acknowledged-extensions.ts";
|
|
7
|
+
|
|
8
|
+
describe("runtime acknowledged extension projection", () => {
|
|
9
|
+
it("validates opaque ids without path-like content", () => {
|
|
10
|
+
assert.equal(isRuntimeAcknowledgedExtensionId("com.example-extension:v1"), true);
|
|
11
|
+
assert.equal(isRuntimeAcknowledgedExtensionId(""), false);
|
|
12
|
+
assert.equal(isRuntimeAcknowledgedExtensionId("has space"), false);
|
|
13
|
+
assert.equal(isRuntimeAcknowledgedExtensionId("../secret"), false);
|
|
14
|
+
assert.equal(isRuntimeAcknowledgedExtensionId("path/to/ext"), false);
|
|
15
|
+
assert.equal(isRuntimeAcknowledgedExtensionId("path\\to\\ext"), false);
|
|
16
|
+
assert.equal(isRuntimeAcknowledgedExtensionId("x".repeat(129)), false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("deduplicates, caps ids, and counts omitted valid ids", () => {
|
|
20
|
+
const ids = ["dup", "dup", ...Array.from({ length: 35 }, (_, index) => `ext-${index}`), "bad id"];
|
|
21
|
+
const projected = projectRuntimeAcknowledgedExtensions(ids);
|
|
22
|
+
|
|
23
|
+
assert.equal(projected?.source, "child-runtime");
|
|
24
|
+
assert.equal(projected?.ids.length, 32);
|
|
25
|
+
assert.equal(projected?.ids[0], "dup");
|
|
26
|
+
assert.equal(projected?.omitted, 4);
|
|
27
|
+
assert.equal(projected?.ids.includes("bad id"), false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("sanitizes persisted projections and rejects invalid shapes", () => {
|
|
31
|
+
assert.deepEqual(sanitizeRuntimeAcknowledgedExtensions({ version: 1, source: "child-runtime", ids: ["ok", "ok", "bad/path"], omitted: 2 }), {
|
|
32
|
+
version: 1,
|
|
33
|
+
source: "child-runtime",
|
|
34
|
+
ids: ["ok"],
|
|
35
|
+
omitted: 2,
|
|
36
|
+
});
|
|
37
|
+
assert.equal(sanitizeRuntimeAcknowledgedExtensions({ version: 1, source: "launch-resolved", ids: ["ok"] }), undefined);
|
|
38
|
+
assert.equal(sanitizeRuntimeAcknowledgedExtensions({ version: 1, source: "child-runtime", ids: ["bad/path"] }), undefined);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects oversized capture files without reading them", (t) => {
|
|
42
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "runtime-ack-"));
|
|
43
|
+
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
44
|
+
const filePath = path.join(dir, "ack.json");
|
|
45
|
+
|
|
46
|
+
fs.writeFileSync(filePath, "x".repeat(MAX_RUNTIME_ACKNOWLEDGED_EXTENSION_FILE_BYTES + 1));
|
|
47
|
+
assert.equal(readRuntimeAcknowledgedExtensions(filePath), undefined);
|
|
48
|
+
|
|
49
|
+
fs.writeFileSync(filePath, JSON.stringify({ version: 1, source: "child-runtime", ids: ["ok"], omitted: 0 }));
|
|
50
|
+
assert.deepEqual(readRuntimeAcknowledgedExtensions(filePath), { version: 1, source: "child-runtime", ids: ["ok"], omitted: 0 });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -38,6 +38,13 @@ interface SubagentParamsSchema {
|
|
|
38
38
|
graceTurns?: { minimum?: number };
|
|
39
39
|
};
|
|
40
40
|
};
|
|
41
|
+
usageBudget?: {
|
|
42
|
+
properties?: {
|
|
43
|
+
tokens?: { properties?: { soft?: { exclusiveMinimum?: number }; hard?: { exclusiveMinimum?: number } } };
|
|
44
|
+
costUsd?: { properties?: { soft?: { exclusiveMinimum?: number }; hard?: { exclusiveMinimum?: number } } };
|
|
45
|
+
};
|
|
46
|
+
description?: string;
|
|
47
|
+
};
|
|
41
48
|
id?: {
|
|
42
49
|
type?: string;
|
|
43
50
|
description?: string;
|
|
@@ -206,6 +213,19 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
206
213
|
assert.equal(toolBudgetSchema?.properties?.hard?.minimum, 1);
|
|
207
214
|
});
|
|
208
215
|
|
|
216
|
+
it("includes root-only reported usage budget", () => {
|
|
217
|
+
const usageBudgetSchema = SubagentParams?.properties?.usageBudget;
|
|
218
|
+
assert.ok(usageBudgetSchema, "usageBudget schema should exist");
|
|
219
|
+
assert.equal(usageBudgetSchema.properties?.tokens?.properties?.soft?.exclusiveMinimum, 0);
|
|
220
|
+
assert.equal(usageBudgetSchema.properties?.tokens?.properties?.hard?.exclusiveMinimum, 0);
|
|
221
|
+
assert.equal(usageBudgetSchema.properties?.costUsd?.properties?.soft?.exclusiveMinimum, 0);
|
|
222
|
+
assert.equal(usageBudgetSchema.properties?.costUsd?.properties?.hard?.exclusiveMinimum, 0);
|
|
223
|
+
assert.match(String(usageBudgetSchema.description ?? ""), /root-only/);
|
|
224
|
+
assert.match(String(usageBudgetSchema.description ?? ""), /running children are not stopped/i);
|
|
225
|
+
assert.equal(getPropertySchema(SubagentParams?.properties?.tasks?.items as JsonSchemaNode | undefined, ["usageBudget"]), undefined);
|
|
226
|
+
assert.equal(getPropertySchema(SubagentParams?.properties?.chain?.items as JsonSchemaNode | undefined, ["usageBudget"]), undefined);
|
|
227
|
+
});
|
|
228
|
+
|
|
209
229
|
it("includes subagent control fields", () => {
|
|
210
230
|
const idSchema = SubagentParams?.properties?.id;
|
|
211
231
|
assert.ok(idSchema, "id schema should exist");
|
|
@@ -214,6 +234,12 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
214
234
|
assert.match(String(idSchema.description ?? ""), /interrupt/i);
|
|
215
235
|
assert.match(String(idSchema.description ?? ""), /steer/i);
|
|
216
236
|
assert.match(String(idSchema.description ?? ""), /append-step/i);
|
|
237
|
+
assert.match(String(idSchema.description ?? ""), /approve-checkpoint/i);
|
|
238
|
+
assert.match(String(idSchema.description ?? ""), /reject-checkpoint/i);
|
|
239
|
+
|
|
240
|
+
const chainItemSchema = SubagentParams?.properties?.chain?.items;
|
|
241
|
+
assert.equal(chainItemSchema?.properties?.checkpoint?.type, "string");
|
|
242
|
+
assert.equal(chainItemSchema?.properties?.message?.type, "string");
|
|
217
243
|
|
|
218
244
|
const runIdSchema = SubagentParams?.properties?.runId;
|
|
219
245
|
assert.ok(runIdSchema, "runId schema should exist");
|
|
@@ -318,12 +344,18 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
318
344
|
assert.ok(SubagentParams, "SubagentParams schema should exist");
|
|
319
345
|
const schema = SubagentParams as unknown as JsonSchemaNode;
|
|
320
346
|
const serialized = JSON.stringify(schema);
|
|
321
|
-
assert.ok(serialized.length <
|
|
347
|
+
assert.ok(serialized.length < 16_000, `expected compact schema under 16k chars, got ${serialized.length}`);
|
|
322
348
|
assert.equal(serialized.includes('"$ref"'), false);
|
|
323
349
|
assert.equal(serialized.includes('"$defs"'), false);
|
|
324
350
|
assert.equal(serialized.split("Optional acceptance policy.").length - 1, 1);
|
|
325
351
|
assert.match(String((schema.properties as Record<string, JsonSchemaNode> | undefined)?.agent?.description ?? ""), /SINGLE mode/);
|
|
326
|
-
|
|
352
|
+
const acceptanceDescription = String((schema.properties as Record<string, JsonSchemaNode> | undefined)?.acceptance?.description ?? "");
|
|
353
|
+
assert.match(acceptanceDescription, /acceptance policy/);
|
|
354
|
+
assert.match(acceptanceDescription, /Supported evidence kinds:/);
|
|
355
|
+
assert.match(acceptanceDescription, /commands-run/);
|
|
356
|
+
assert.match(acceptanceDescription, /changed-files/);
|
|
357
|
+
assert.match(acceptanceDescription, /manual-notes/);
|
|
358
|
+
assert.match(acceptanceDescription, /\{ level: "checked", evidence: \["commands-run", "changed-files"\] \}/);
|
|
327
359
|
|
|
328
360
|
const nestedDescriptionPaths: string[] = [];
|
|
329
361
|
const stack: Array<{ path: string; value: unknown }> = [{ path: "SubagentParams", value: schema }];
|
|
@@ -340,7 +340,7 @@ describe("skills filesystem fallback", () => {
|
|
|
340
340
|
`;
|
|
341
341
|
execFileSync(
|
|
342
342
|
process.execPath,
|
|
343
|
-
["--experimental-
|
|
343
|
+
["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script],
|
|
344
344
|
{
|
|
345
345
|
cwd: projectRoot,
|
|
346
346
|
env: {
|
|
@@ -60,7 +60,7 @@ function projectRequest(status: AsyncStatus, request: SteerRequest, states: Stee
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
async function waitUntil<T>(read: () => T | undefined, timeoutMs =
|
|
63
|
+
async function waitUntil<T>(read: () => T | undefined, timeoutMs = 30_000): Promise<T> {
|
|
64
64
|
const deadline = Date.now() + timeoutMs;
|
|
65
65
|
while (true) {
|
|
66
66
|
const value = read();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { describe, it } from "node:test";
|
|
4
|
+
import {
|
|
5
|
+
boundStreamedRecentOutput,
|
|
6
|
+
boundStreamedRecentTools,
|
|
7
|
+
boundStreamedToolCalls,
|
|
8
|
+
MAX_STREAMED_OUTPUT_LINE_CHARS,
|
|
9
|
+
MAX_STREAMED_RECENT_TOOLS,
|
|
10
|
+
MAX_STREAMED_TOOL_CALLS,
|
|
11
|
+
} from "../../src/shared/utils.ts";
|
|
12
|
+
|
|
13
|
+
describe("streamed progress snapshot bounds", () => {
|
|
14
|
+
it("keeps only the most recent tool-history entries and clones them", () => {
|
|
15
|
+
const recentTools = Array.from({ length: MAX_STREAMED_RECENT_TOOLS + 40 }, (_, i) => ({
|
|
16
|
+
tool: "read",
|
|
17
|
+
args: `file-${i}.ts`,
|
|
18
|
+
endMs: i,
|
|
19
|
+
}));
|
|
20
|
+
const bounded = boundStreamedRecentTools(recentTools);
|
|
21
|
+
assert.equal(bounded.length, MAX_STREAMED_RECENT_TOOLS);
|
|
22
|
+
// most-recent retained (chronological)
|
|
23
|
+
assert.equal(bounded.at(-1)?.endMs, recentTools.at(-1)?.endMs);
|
|
24
|
+
assert.equal(bounded[0]?.endMs, recentTools.at(-MAX_STREAMED_RECENT_TOOLS)?.endMs);
|
|
25
|
+
// cloned, not aliased
|
|
26
|
+
bounded[0]!.args = "mutated";
|
|
27
|
+
assert.notEqual(recentTools.at(-MAX_STREAMED_RECENT_TOOLS)?.args, "mutated");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("truncates long recent-output lines but leaves short lines intact", () => {
|
|
31
|
+
const shortLine = "ok";
|
|
32
|
+
const longLine = "x".repeat(MAX_STREAMED_OUTPUT_LINE_CHARS + 5000);
|
|
33
|
+
const bounded = boundStreamedRecentOutput([shortLine, longLine]);
|
|
34
|
+
assert.equal(bounded[0], shortLine);
|
|
35
|
+
assert.ok(bounded[1]!.length < longLine.length);
|
|
36
|
+
assert.ok(bounded[1]!.startsWith("x".repeat(MAX_STREAMED_OUTPUT_LINE_CHARS)));
|
|
37
|
+
assert.match(bounded[1]!, /\[truncated\]$/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("prefers an existing toolCalls summary and caps it", () => {
|
|
41
|
+
const toolCalls = Array.from({ length: MAX_STREAMED_TOOL_CALLS + 20 }, (_, i) => ({
|
|
42
|
+
text: `read(${i})`,
|
|
43
|
+
expandedText: `read(file-${i}.ts)`,
|
|
44
|
+
}));
|
|
45
|
+
const bounded = boundStreamedToolCalls({ toolCalls, messages: undefined });
|
|
46
|
+
assert.equal(bounded?.length, MAX_STREAMED_TOOL_CALLS);
|
|
47
|
+
assert.equal(bounded?.at(-1)?.text, toolCalls.at(-1)?.text);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("derives tool-call summaries from messages when none are present", () => {
|
|
51
|
+
const messages = [
|
|
52
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "a.ts" } }] },
|
|
53
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "grep", arguments: { pattern: "foo" } }] },
|
|
54
|
+
{ role: "user", content: [{ type: "text", text: "ignored" }] },
|
|
55
|
+
] as never;
|
|
56
|
+
const bounded = boundStreamedToolCalls({ toolCalls: undefined, messages });
|
|
57
|
+
assert.equal(bounded?.length, 2);
|
|
58
|
+
assert.match(bounded![0]!.text, /read/);
|
|
59
|
+
assert.match(bounded![1]!.text, /grep/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns undefined when there are no tool calls", () => {
|
|
63
|
+
assert.equal(boundStreamedToolCalls({ toolCalls: [], messages: [] as never }), undefined);
|
|
64
|
+
assert.equal(boundStreamedToolCalls({ toolCalls: undefined, messages: undefined }), undefined);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("bounds a pathological running snapshot well under the child-stdout protocol cap", () => {
|
|
68
|
+
// Simulates a long, deep nested fan-out: many tool calls + long output lines.
|
|
69
|
+
const recentTools = Array.from({ length: 5000 }, (_, i) => ({ tool: "read", args: `services/pkg/file-${i}.ts`, endMs: i }));
|
|
70
|
+
const recentOutput = Array.from({ length: 50 }, () => "y".repeat(60_000));
|
|
71
|
+
const snapshot = {
|
|
72
|
+
recentTools: boundStreamedRecentTools(recentTools),
|
|
73
|
+
recentOutput: boundStreamedRecentOutput(recentOutput),
|
|
74
|
+
};
|
|
75
|
+
const bytes = Buffer.byteLength(JSON.stringify(snapshot), "utf8");
|
|
76
|
+
assert.ok(bytes < 256 * 1024, `bounded snapshot should be small, was ${bytes} bytes`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
SUBAGENT_STEER_INBOX_ENV,
|
|
17
17
|
SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,
|
|
18
18
|
} from "../../src/runs/shared/pi-args.ts";
|
|
19
|
+
import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV } from "../../src/runs/shared/runtime-acknowledged-extensions.ts";
|
|
19
20
|
import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "../../src/runs/shared/structured-output.ts";
|
|
20
21
|
import { TOOL_BUDGET_ENV } from "../../src/runs/shared/tool-budget.ts";
|
|
21
22
|
import { CHILD_TOOL_DIAGNOSTIC_PATH_ENV, formatChildToolDiagnostic, MCP_DIRECT_CHILD_TOOLS_ENV, readChildToolDiagnostic, REQUIRED_CHILD_TOOLS_ENV } from "../../src/runs/shared/tool-availability.ts";
|
|
@@ -128,6 +129,42 @@ function setSupervisorEnv(): void {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
describe("subagent prompt runtime", () => {
|
|
132
|
+
it("collects runtime extension acknowledgements until terminal serialization", () => {
|
|
133
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-runtime-ack-"));
|
|
134
|
+
try {
|
|
135
|
+
const outputPath = path.join(dir, "acks.json");
|
|
136
|
+
process.env[RUNTIME_EXTENSION_ACK_PATH_ENV] = outputPath;
|
|
137
|
+
const runtimeHandlers = new Map<string, Array<(payload?: unknown) => unknown>>();
|
|
138
|
+
const extensionHandlers = new Map<string, Array<(payload?: unknown) => unknown>>();
|
|
139
|
+
const pushHandler = (target: Map<string, Array<(payload?: unknown) => unknown>>, event: string, handler: (payload?: unknown) => unknown): void => {
|
|
140
|
+
target.set(event, [...(target.get(event) ?? []), handler]);
|
|
141
|
+
};
|
|
142
|
+
const emitAll = (target: Map<string, Array<(payload?: unknown) => unknown>>, event: string, payload?: unknown): void => {
|
|
143
|
+
for (const handler of target.get(event) ?? []) handler(payload);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
registerSubagentPromptRuntime({
|
|
147
|
+
events: { on(event: string, handler: (payload?: unknown) => unknown) { pushHandler(extensionHandlers, event, handler); } },
|
|
148
|
+
on(event: string, handler: (payload?: unknown) => unknown) { pushHandler(runtimeHandlers, event, handler); },
|
|
149
|
+
} as never);
|
|
150
|
+
|
|
151
|
+
emitAll(extensionHandlers, RUNTIME_EXTENSION_ACK_EVENT, { id: "ext.one" });
|
|
152
|
+
emitAll(extensionHandlers, RUNTIME_EXTENSION_ACK_EVENT, { id: "ext.one" });
|
|
153
|
+
emitAll(extensionHandlers, RUNTIME_EXTENSION_ACK_EVENT, { id: "bad/path" });
|
|
154
|
+
runtimeHandlers.get("agent_end")?.[0]?.({});
|
|
155
|
+
emitAll(extensionHandlers, RUNTIME_EXTENSION_ACK_EVENT, { id: "late" });
|
|
156
|
+
|
|
157
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(outputPath, "utf-8")), {
|
|
158
|
+
version: 1,
|
|
159
|
+
source: "child-runtime",
|
|
160
|
+
ids: ["ext.one"],
|
|
161
|
+
omitted: 0,
|
|
162
|
+
});
|
|
163
|
+
} finally {
|
|
164
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
131
168
|
it("nudges after the tool budget soft limit and blocks configured tools after hard", () => {
|
|
132
169
|
const handlers = new Map<string, (payload: { toolName?: string }) => unknown>();
|
|
133
170
|
const sent: string[] = [];
|
|
@@ -299,6 +336,10 @@ describe("subagent prompt runtime", () => {
|
|
|
299
336
|
});
|
|
300
337
|
|
|
301
338
|
it("registers child watchdog lifecycle handlers only when enabled by env", () => {
|
|
339
|
+
delete process.env[CHILD_WATCHDOG_CONFIG_ENV];
|
|
340
|
+
// Clear the ack capture env explicitly: when this test suite itself runs inside a
|
|
341
|
+
// pi-subagents child, the runner sets it and an extra agent_end handler registers.
|
|
342
|
+
delete process.env[RUNTIME_EXTENSION_ACK_PATH_ENV];
|
|
302
343
|
const handlersWithout = new Map<string, unknown[]>();
|
|
303
344
|
registerSubagentPromptRuntime({
|
|
304
345
|
on(event: string, handler: unknown) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { Usage } from "../../src/shared/types.ts";
|
|
4
|
+
import {
|
|
5
|
+
MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS,
|
|
6
|
+
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
7
|
+
formatSubagentStartupRetryExhaustedError,
|
|
8
|
+
formatSubagentStartupRetryNote,
|
|
9
|
+
isRetryableSubagentStartupFailure,
|
|
10
|
+
waitForSubagentStartupRetry,
|
|
11
|
+
type SubagentStartupFailureEvidence,
|
|
12
|
+
} from "../../src/runs/shared/subagent-startup-retry.ts";
|
|
13
|
+
|
|
14
|
+
function emptyUsage(): Usage {
|
|
15
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function startupFailure(overrides: Partial<SubagentStartupFailureEvidence> = {}): SubagentStartupFailureEvidence {
|
|
19
|
+
return {
|
|
20
|
+
exitCode: 1,
|
|
21
|
+
messageCount: 0,
|
|
22
|
+
toolCount: 0,
|
|
23
|
+
usage: emptyUsage(),
|
|
24
|
+
durationMs: 20,
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("subagent startup retry", () => {
|
|
30
|
+
it("uses a bounded retry schedule", () => {
|
|
31
|
+
assert.deepEqual(SUBAGENT_STARTUP_RETRY_DELAYS_MS, [250, 750, 1500]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("classifies a short non-zero exit with no child activity", () => {
|
|
35
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure()), true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("rejects successful, long-running, and diagnosed exits", () => {
|
|
39
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ exitCode: 0 })), false);
|
|
40
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ durationMs: MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS + 1 })), false);
|
|
41
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ error: "authentication failed" })), false);
|
|
42
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ finalOutput: "partial response" })), false);
|
|
43
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ protocolError: { code: "protocol_output_limit" } })), false);
|
|
44
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ processSignal: "SIGKILL" })), false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects any model, tool, usage, mutation, or lifecycle activity", () => {
|
|
48
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ messageCount: 1 })), false);
|
|
49
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ toolCount: 1 })), false);
|
|
50
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ usage: { ...emptyUsage(), input: 1 } })), false);
|
|
51
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ usage: { ...emptyUsage(), turns: 1 } })), false);
|
|
52
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ observedMutationAttempt: true })), false);
|
|
53
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ detached: true })), false);
|
|
54
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ interrupted: true })), false);
|
|
55
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ timedOut: true })), false);
|
|
56
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ stopped: true })), false);
|
|
57
|
+
assert.equal(isRetryableSubagentStartupFailure(startupFailure({ turnBudgetExceeded: true })), false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("formats retry and exhaustion diagnostics without task content", () => {
|
|
61
|
+
assert.equal(
|
|
62
|
+
formatSubagentStartupRetryNote({ model: "openai/test", attempt: 1, maxAttempts: 4, delayMs: 250 }),
|
|
63
|
+
"[startup-retry] openai/test exited before model or tool activity (attempt 1/4). Retrying the same model in 250ms.",
|
|
64
|
+
);
|
|
65
|
+
assert.match(
|
|
66
|
+
formatSubagentStartupRetryExhaustedError({ model: "openai/test", attempts: 4 }),
|
|
67
|
+
/failed to start after 4 attempts.*concurrent Pi startup race.*lower subagent concurrency/i,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("cancels a pending retry delay", async () => {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const wait = waitForSubagentStartupRetry(5000, [controller.signal]);
|
|
74
|
+
controller.abort();
|
|
75
|
+
assert.equal(await wait, false);
|
|
76
|
+
});
|
|
77
|
+
});
|