@selesai/code 0.5.28 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/dist/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 +19 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/system-prompt.test.d.ts +2 -0
- package/dist/core/system-prompt.test.d.ts.map +1 -0
- package/dist/core/system-prompt.test.js +89 -0
- package/dist/core/system-prompt.test.js.map +1 -0
- package/dist/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/defaults/models.json +13 -45
- package/dist/defaults/settings.json +1 -2
- package/dist/extensions/copy-turn.test.ts +131 -0
- package/dist/extensions/copy-turn.ts +6 -1
- package/dist/extensions/package.json +0 -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 +88 -0
- package/dist/extensions/pi-subagents/LICENSE +21 -0
- package/dist/extensions/pi-subagents/README.md +158 -69
- package/dist/extensions/pi-subagents/agents/architect.md +5 -4
- package/dist/extensions/pi-subagents/agents/builder.md +6 -4
- package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
- package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
- package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
- package/dist/extensions/pi-subagents/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 +22 -988
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +431 -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 +281 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +125 -37
- 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/agents/task-aware-routing.ts +125 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +17 -13
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +22 -8
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +33 -8
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +32 -14
- 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 +86 -21
- 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 +56 -5
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +92 -11
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +388 -29
- 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 +214 -60
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +546 -252
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +553 -154
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +19 -13
- 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/single-output.ts +63 -9
- 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 +22 -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 +196 -12
- package/dist/extensions/pi-subagents/src/shared/utils.ts +102 -5
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +10 -1
- 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 +55 -34
- 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 +113 -8
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +419 -47
- 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 +62 -22
- 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 +8 -6
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +231 -19
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +48 -7
- 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 +199 -17
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +898 -16
- 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/real-session-runner.ts +18 -2
- 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-disabled.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +198 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
- 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/builtin-agent-documentation.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +136 -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 +28 -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 +17 -12
- 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 +39 -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 +48 -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 +46 -2
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
- 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-aware-routing.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +61 -10
- 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/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/ponytail/SKILL.md +1 -3
- 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/plans/subagent-delegation/phase-0-correctness.md +265 -0
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
- package/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 +16 -7
- package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
- package/dist/extensions/caveman/index.js +0 -118
- package/dist/extensions/caveman/package.json +0 -8
- package/dist/extensions/caveman/test/extension.test.js +0 -203
- package/dist/extensions/caveman/test/helpers.test.js +0 -58
- package/dist/skills/caveman/SKILL.md +0 -50
|
@@ -15,7 +15,7 @@ import * as os from "node:os";
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import { createEventBus, createMockPi, createTempDir, events, makeAgent, makeMinimalCtx, removeTempDir, tryImport } from "../support/helpers.ts";
|
|
17
17
|
import type { MockPi } from "../support/helpers.ts";
|
|
18
|
-
import { deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest } from "../../src/runs/background/control-channel.ts";
|
|
18
|
+
import { deliverCheckpointDecisionRequest, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest } from "../../src/runs/background/control-channel.ts";
|
|
19
19
|
import { waitForSubagents } from "../../src/runs/background/subagent-wait.ts";
|
|
20
20
|
import { writeAtomicJson } from "../../src/shared/atomic-json.ts";
|
|
21
21
|
import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
|
|
@@ -26,10 +26,35 @@ import { resolveSubagentLaunchContract } from "../../src/api/preflight.ts";
|
|
|
26
26
|
import { discoverAgents } from "../../src/agents/agents.ts";
|
|
27
27
|
import { runSync } from "../../src/runs/foreground/execution.ts";
|
|
28
28
|
|
|
29
|
+
interface LaunchResolvedExtensions {
|
|
30
|
+
version?: number;
|
|
31
|
+
source?: string;
|
|
32
|
+
disableAmbientExtensions?: boolean;
|
|
33
|
+
runtime?: string[];
|
|
34
|
+
configured?: string[];
|
|
35
|
+
effective?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RuntimeAcknowledgedExtensions {
|
|
39
|
+
version?: number;
|
|
40
|
+
source?: string;
|
|
41
|
+
ids?: string[];
|
|
42
|
+
omitted?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface UsageBudgetState {
|
|
46
|
+
version?: number;
|
|
47
|
+
source?: string;
|
|
48
|
+
exhausted?: boolean;
|
|
49
|
+
reason?: string;
|
|
50
|
+
tokens?: { used?: number; hard?: number; exhausted?: boolean };
|
|
51
|
+
costUsd?: { used?: number; hard?: number; exhausted?: boolean };
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
interface AsyncExecutionResult {
|
|
30
55
|
content: Array<{ text?: string }>;
|
|
31
56
|
isError?: boolean;
|
|
32
|
-
details: { asyncId?: string; asyncDir?: string; launchContractDigest?: string };
|
|
57
|
+
details: { asyncId?: string; asyncDir?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; usageBudget?: UsageBudgetState; checkpoint?: { name?: string; status?: string } };
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
interface AsyncResultPayload {
|
|
@@ -40,6 +65,8 @@ interface AsyncResultPayload {
|
|
|
40
65
|
sessionId?: string;
|
|
41
66
|
mode?: string;
|
|
42
67
|
launchContractDigest?: string;
|
|
68
|
+
launchResolvedExtensions?: LaunchResolvedExtensions;
|
|
69
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
|
|
43
70
|
summary?: string;
|
|
44
71
|
error?: string;
|
|
45
72
|
timeoutMs?: number;
|
|
@@ -51,7 +78,9 @@ interface AsyncResultPayload {
|
|
|
51
78
|
wrapUpRequested?: boolean;
|
|
52
79
|
totalTokens?: { input: number; output: number; total: number };
|
|
53
80
|
totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
|
|
54
|
-
|
|
81
|
+
usageBudget?: UsageBudgetState;
|
|
82
|
+
checkpoint?: { name?: string; status?: string };
|
|
83
|
+
results: Array<{ agent?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputPath?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
|
|
55
84
|
outputs?: Record<string, { text?: string; structured?: unknown }>;
|
|
56
85
|
workflowGraph?: { nodes?: Array<{ kind?: string; label?: string; phase?: string; status?: string; acceptanceStatus?: string; error?: string; outputName?: string; structured?: boolean; children?: Array<{ label?: string; outputName?: string; itemKey?: string; status?: string; acceptanceStatus?: string; error?: string }> }> };
|
|
57
86
|
parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
|
|
@@ -68,6 +97,8 @@ interface AsyncStatusPayload {
|
|
|
68
97
|
currentPath?: string;
|
|
69
98
|
state?: string;
|
|
70
99
|
launchContractDigest?: string;
|
|
100
|
+
launchResolvedExtensions?: LaunchResolvedExtensions;
|
|
101
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
|
|
71
102
|
error?: string;
|
|
72
103
|
timeoutMs?: number;
|
|
73
104
|
deadlineAt?: number;
|
|
@@ -78,6 +109,8 @@ interface AsyncStatusPayload {
|
|
|
78
109
|
wrapUpRequested?: boolean;
|
|
79
110
|
totalTokens?: { total: number };
|
|
80
111
|
totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
|
|
112
|
+
usageBudget?: UsageBudgetState;
|
|
113
|
+
checkpoint?: { name?: string; status?: string; message?: string };
|
|
81
114
|
parallelGroups?: Array<{ start: number; count: number; stepIndex: number }>;
|
|
82
115
|
parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
|
|
83
116
|
capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] };
|
|
@@ -100,6 +133,8 @@ interface AsyncStatusPayload {
|
|
|
100
133
|
totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
|
|
101
134
|
agentContract?: { version: 1 };
|
|
102
135
|
launchContractDigest?: string;
|
|
136
|
+
launchResolvedExtensions?: LaunchResolvedExtensions;
|
|
137
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
|
|
103
138
|
execution?: { status?: string; success?: boolean; exitCode?: number };
|
|
104
139
|
effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } };
|
|
105
140
|
acceptance?: { status?: string };
|
|
@@ -292,6 +327,19 @@ async function waitForAsyncResultFile(id: string, timeoutMs = 15_000): Promise<s
|
|
|
292
327
|
return resultPath;
|
|
293
328
|
}
|
|
294
329
|
|
|
330
|
+
async function waitForAsyncState(id: string, predicate: (status: AsyncStatusPayload) => boolean, timeoutMs = 10_000): Promise<AsyncStatusPayload> {
|
|
331
|
+
const statusPath = path.join(ASYNC_DIR, id, "status.json");
|
|
332
|
+
const deadline = Date.now() + timeoutMs;
|
|
333
|
+
while (Date.now() <= deadline) {
|
|
334
|
+
if (fs.existsSync(statusPath)) {
|
|
335
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
|
|
336
|
+
if (predicate(status)) return status;
|
|
337
|
+
}
|
|
338
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
339
|
+
}
|
|
340
|
+
assert.fail(`Timed out waiting for async status: ${statusPath}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
295
343
|
async function waitForDeferredTurnBudget(id: string, timeoutMs = 10_000): Promise<AsyncStatusPayload> {
|
|
296
344
|
const statusPath = path.join(ASYNC_DIR, id, "status.json");
|
|
297
345
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -405,6 +453,18 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
405
453
|
return JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
406
454
|
}
|
|
407
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Reference-first async child assertion: the result payload carries the
|
|
458
|
+
* saved-output reference; the full text is read from the durable output path.
|
|
459
|
+
*/
|
|
460
|
+
function assertAsyncChildOutput(payload: AsyncResultPayload, index: number, expected: string): void {
|
|
461
|
+
const child = payload.results[index];
|
|
462
|
+
assert.ok(child, `expected async child ${index}`);
|
|
463
|
+
assert.match(child.output ?? "", /Output saved to: /);
|
|
464
|
+
assert.ok(child.outputPath, `expected a durable output path for child ${index}`);
|
|
465
|
+
assert.equal(fs.readFileSync(child.outputPath, "utf-8"), expected);
|
|
466
|
+
}
|
|
467
|
+
|
|
408
468
|
function launchProtocolTest(id: string): void {
|
|
409
469
|
executeAsyncSingle(id, {
|
|
410
470
|
agent: "worker",
|
|
@@ -432,7 +492,27 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
432
492
|
launchProtocolTest(id);
|
|
433
493
|
const payload = await readAsyncPayload(id);
|
|
434
494
|
assert.equal(payload.success, true);
|
|
435
|
-
|
|
495
|
+
assertAsyncChildOutput(payload, 0, "你好 from fragmented async JSON");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("persists absent output provenance when async lifecycle text is synthetic", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
499
|
+
mockPi.onCall({ jsonl: [mockAssistantMessage("", "tool_use")], stderr: "mock child failure", exitCode: 1 });
|
|
500
|
+
const id = `async-output-absent-${Date.now().toString(36)}`;
|
|
501
|
+
launchProtocolTest(id);
|
|
502
|
+
const payload = await readAsyncPayload(id);
|
|
503
|
+
assert.equal(payload.success, false);
|
|
504
|
+
assert.equal(payload.results[0]?.outputState, "absent");
|
|
505
|
+
assert.match(payload.results[0]?.error ?? "", /mock child failure/);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("persists present output provenance when async failure follows partial output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
509
|
+
mockPi.onCall({ output: "usable partial answer", stderr: "mock post-output failure", exitCode: 1 });
|
|
510
|
+
const id = `async-output-present-${Date.now().toString(36)}`;
|
|
511
|
+
launchProtocolTest(id);
|
|
512
|
+
const payload = await readAsyncPayload(id);
|
|
513
|
+
assert.equal(payload.success, false);
|
|
514
|
+
assert.equal(payload.results[0]?.outputState, "present");
|
|
515
|
+
assertAsyncChildOutput(payload, 0, "usable partial answer");
|
|
436
516
|
});
|
|
437
517
|
|
|
438
518
|
it("matches preflight launch digest in equivalent foreground and async execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -444,11 +524,14 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
444
524
|
fs.writeFileSync(agentPath, `---\nname: ${agentName}\ndescription: Contract comparison worker\n---\n`, "utf-8");
|
|
445
525
|
const discovered = discoverAgents(tempDir).agents.find((agent) => agent.name === agentName);
|
|
446
526
|
assert.ok(discovered, "expected temporary agent definition to be discovered");
|
|
447
|
-
|
|
527
|
+
// Stable explicit output keeps the preflight launch contract equivalent to
|
|
528
|
+
// both execution paths (generated per-run paths would differ by design).
|
|
529
|
+
const contractOutputPath = path.join(tempDir, "contract-output.md");
|
|
530
|
+
const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, turnBudget, runId: "contract-preflight", output: contractOutputPath });
|
|
448
531
|
assert.equal(preflight.ok, true);
|
|
449
532
|
|
|
450
533
|
mockPi.onCall({ output: "foreground contract comparison" });
|
|
451
|
-
const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget });
|
|
534
|
+
const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget, outputPath: contractOutputPath });
|
|
452
535
|
assert.equal(foreground.exitCode, 0);
|
|
453
536
|
assert.equal(foreground.launchContractDigest, preflight.contract.launchContractDigest);
|
|
454
537
|
|
|
@@ -457,6 +540,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
457
540
|
const launch = executeAsyncSingle(asyncId, {
|
|
458
541
|
agent: agentName,
|
|
459
542
|
task,
|
|
543
|
+
output: contractOutputPath,
|
|
544
|
+
outputMode: "inline",
|
|
460
545
|
agentConfig: discovered,
|
|
461
546
|
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
462
547
|
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
@@ -473,12 +558,16 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
473
558
|
});
|
|
474
559
|
|
|
475
560
|
it("persists the actual launch digest in async status and result metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
476
|
-
mockPi.onCall({
|
|
561
|
+
mockPi.onCall({
|
|
562
|
+
output: "digest-bound async done",
|
|
563
|
+
runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", ids: ["ext.async"], omitted: 0 },
|
|
564
|
+
});
|
|
477
565
|
const id = `async-launch-digest-${Date.now().toString(36)}`;
|
|
566
|
+
const privateExtension = path.join(tempDir, "extensions", "private-extension.ts");
|
|
478
567
|
const launch = executeAsyncSingle(id, {
|
|
479
568
|
agent: "worker",
|
|
480
569
|
task: "Exercise launch digest reporting",
|
|
481
|
-
agentConfig: makeAgent("worker", { completionGuard: false }),
|
|
570
|
+
agentConfig: makeAgent("worker", { completionGuard: false, extensions: [privateExtension] }),
|
|
482
571
|
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
483
572
|
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
484
573
|
shareEnabled: false,
|
|
@@ -493,6 +582,140 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
493
582
|
assert.equal(payload.results[0]?.launchContractDigest, launch.details.launchContractDigest);
|
|
494
583
|
assert.equal(status.launchContractDigest, launch.details.launchContractDigest);
|
|
495
584
|
assert.equal(status.steps?.[0]?.launchContractDigest, launch.details.launchContractDigest);
|
|
585
|
+
assert.equal(launch.details.launchResolvedExtensions?.source, "launch-resolved");
|
|
586
|
+
assert.equal(launch.details.launchResolvedExtensions?.disableAmbientExtensions, true);
|
|
587
|
+
assert.deepEqual(payload.launchResolvedExtensions, launch.details.launchResolvedExtensions);
|
|
588
|
+
assert.deepEqual(payload.results[0]?.launchResolvedExtensions, launch.details.launchResolvedExtensions);
|
|
589
|
+
assert.deepEqual(status.launchResolvedExtensions, launch.details.launchResolvedExtensions);
|
|
590
|
+
assert.deepEqual(status.steps?.[0]?.launchResolvedExtensions, launch.details.launchResolvedExtensions);
|
|
591
|
+
const runtimeAck = { version: 1, source: "child-runtime", ids: ["ext.async"], omitted: 0 };
|
|
592
|
+
assert.deepEqual(payload.runtimeAcknowledgedExtensions, runtimeAck);
|
|
593
|
+
assert.deepEqual(payload.results[0]?.runtimeAcknowledgedExtensions, runtimeAck);
|
|
594
|
+
assert.deepEqual(status.runtimeAcknowledgedExtensions, runtimeAck);
|
|
595
|
+
assert.deepEqual(status.steps?.[0]?.runtimeAcknowledgedExtensions, runtimeAck);
|
|
596
|
+
assert.ok(!JSON.stringify(launch.details.launchResolvedExtensions).includes(tempDir), "projection should not expose raw extension paths");
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("async chain checkpoints pause before the next child until approved", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
600
|
+
mockPi.onCall({ output: "analysis done" });
|
|
601
|
+
mockPi.onCall({ output: "implementation done" });
|
|
602
|
+
const id = `async-checkpoint-approve-${Date.now().toString(36)}`;
|
|
603
|
+
const launch = executeAsyncChain(id, {
|
|
604
|
+
chain: [
|
|
605
|
+
{ agent: "analyst", task: "Analyze" },
|
|
606
|
+
{ checkpoint: "review", message: "Approve implementation?" },
|
|
607
|
+
{ agent: "reporter", task: "Summarize {previous}" },
|
|
608
|
+
],
|
|
609
|
+
agents: [makeAgent("analyst"), makeAgent("reporter")],
|
|
610
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
611
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
612
|
+
shareEnabled: false,
|
|
613
|
+
maxSubagentDepth: 2,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const paused = await waitForAsyncState(id, (status) => status.state === "paused" && status.checkpoint?.name === "review");
|
|
617
|
+
assert.equal(launch.isError, undefined);
|
|
618
|
+
assert.equal(paused.steps?.[1]?.status, "paused");
|
|
619
|
+
assert.equal(mockPi.callCount(), 1);
|
|
620
|
+
|
|
621
|
+
deliverCheckpointDecisionRequest({ asyncDir: path.join(ASYNC_DIR, id), decision: "approved" });
|
|
622
|
+
const payload = await readAsyncPayload(id);
|
|
623
|
+
assert.equal(payload.success, true);
|
|
624
|
+
assert.equal(payload.state, "complete");
|
|
625
|
+
assert.equal(payload.checkpoint?.status, "approved");
|
|
626
|
+
assert.equal(payload.results.length, 2);
|
|
627
|
+
assert.equal(mockPi.callCount(), 2);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("foreground checkpoint decisions use current-session control actions", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
|
|
631
|
+
mockPi.onCall({ output: "analysis done" });
|
|
632
|
+
const executor = makeAsyncExecutor([makeAgent("analyst"), makeAgent("worker")]);
|
|
633
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
634
|
+
ctx.sessionManager.getSessionId = () => "session-foreground-checkpoint";
|
|
635
|
+
const id = `foreground-checkpoint-${Date.now().toString(36)}`;
|
|
636
|
+
|
|
637
|
+
const paused = await executor.execute(id, {
|
|
638
|
+
chain: [
|
|
639
|
+
{ agent: "analyst", task: "Analyze" },
|
|
640
|
+
{ checkpoint: "review", message: "Approve implementation?" },
|
|
641
|
+
{ agent: "worker", task: "Implement" },
|
|
642
|
+
],
|
|
643
|
+
}, new AbortController().signal, undefined, ctx) as AsyncExecutionResult;
|
|
644
|
+
|
|
645
|
+
assert.equal(paused.isError, undefined);
|
|
646
|
+
assert.equal(paused.details.checkpoint?.status, "pending");
|
|
647
|
+
assert.equal(mockPi.callCount(), 1);
|
|
648
|
+
const runId = (paused.details as AsyncExecutionResult["details"] & { runId?: string }).runId;
|
|
649
|
+
assert.ok(runId);
|
|
650
|
+
|
|
651
|
+
const approved = await executor.execute(`${id}-approve`, { action: "approve-checkpoint", id: runId }, new AbortController().signal, undefined, ctx) as AsyncExecutionResult;
|
|
652
|
+
assert.equal(approved.isError, undefined);
|
|
653
|
+
assert.equal(approved.details.checkpoint?.name, "review");
|
|
654
|
+
assert.equal(approved.details.checkpoint?.status, "approved");
|
|
655
|
+
assert.equal(mockPi.callCount(), 1);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("async chain checkpoint rejection is an explicit terminal state", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
659
|
+
mockPi.onCall({ output: "analysis done" });
|
|
660
|
+
mockPi.onCall({ output: "should not run" });
|
|
661
|
+
const id = `async-checkpoint-reject-${Date.now().toString(36)}`;
|
|
662
|
+
executeAsyncChain(id, {
|
|
663
|
+
chain: [
|
|
664
|
+
{ agent: "analyst", task: "Analyze" },
|
|
665
|
+
{ checkpoint: "review" },
|
|
666
|
+
{ agent: "worker", task: "Implement" },
|
|
667
|
+
],
|
|
668
|
+
agents: [makeAgent("analyst"), makeAgent("worker")],
|
|
669
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
670
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
671
|
+
shareEnabled: false,
|
|
672
|
+
maxSubagentDepth: 2,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
await waitForAsyncState(id, (status) => status.state === "paused" && status.checkpoint?.name === "review");
|
|
676
|
+
deliverCheckpointDecisionRequest({ asyncDir: path.join(ASYNC_DIR, id), decision: "rejected" });
|
|
677
|
+
const payload = await readAsyncPayload(id);
|
|
678
|
+
assert.equal(payload.success, false);
|
|
679
|
+
assert.equal(payload.state, "rejected");
|
|
680
|
+
assert.equal(payload.checkpoint?.status, "rejected");
|
|
681
|
+
assert.equal(mockPi.callCount(), 1);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("background parallel groups report usage budget state and block queued children", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
685
|
+
mockPi.onCall({ output: "first async result" });
|
|
686
|
+
const id = `async-usage-budget-${Date.now().toString(36)}`;
|
|
687
|
+
const launch = executeAsyncChain(id, {
|
|
688
|
+
chain: [{
|
|
689
|
+
parallel: [
|
|
690
|
+
{ agent: "first", task: "First task" },
|
|
691
|
+
{ agent: "second", task: "Second task" },
|
|
692
|
+
],
|
|
693
|
+
concurrency: 1,
|
|
694
|
+
}],
|
|
695
|
+
resultMode: "parallel",
|
|
696
|
+
usageBudget: { tokens: { hard: 10 } },
|
|
697
|
+
agents: [makeAgent("first"), makeAgent("second")],
|
|
698
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
699
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
700
|
+
shareEnabled: false,
|
|
701
|
+
maxSubagentDepth: 2,
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
assert.equal(launch.details.usageBudget?.exhausted, false);
|
|
705
|
+
const payload = await readAsyncPayload(id);
|
|
706
|
+
const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
|
|
707
|
+
assert.equal(payload.success, false);
|
|
708
|
+
assert.equal(payload.state, "failed");
|
|
709
|
+
assert.match(payload.error ?? payload.summary ?? "", /Usage budget exhausted/);
|
|
710
|
+
assert.equal(payload.results.length, 2);
|
|
711
|
+
assert.equal(payload.results[1]?.skipped, true);
|
|
712
|
+
assert.match(payload.results[1]?.error ?? "", /Usage budget exhausted/);
|
|
713
|
+
assert.equal(mockPi.callCount(), 1);
|
|
714
|
+
assert.equal(payload.usageBudget?.exhausted, true);
|
|
715
|
+
assert.equal(payload.usageBudget?.reason, "tokens");
|
|
716
|
+
assert.equal(status.usageBudget?.exhausted, true);
|
|
717
|
+
assert.equal(status.steps?.[0]?.status, "complete");
|
|
718
|
+
assert.equal(status.steps?.[1]?.status, "failed");
|
|
496
719
|
});
|
|
497
720
|
|
|
498
721
|
it("background fails with protocol_output_limit for an oversized stdout line", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -515,7 +738,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
515
738
|
|
|
516
739
|
const launch = await executor.execute(
|
|
517
740
|
"async-session-artifact-dir",
|
|
518
|
-
{ agent: "worker", task: "Write async session artifacts", async: true, runId: "async-session-artifacts", acceptance: false },
|
|
741
|
+
{ agent: "worker", task: "Write async session artifacts", async: true, runId: "async-session-artifacts", acceptance: false, artifacts: true },
|
|
519
742
|
new AbortController().signal,
|
|
520
743
|
undefined,
|
|
521
744
|
ctx,
|
|
@@ -623,7 +846,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
623
846
|
launchProtocolTest(id);
|
|
624
847
|
const payload = await readAsyncPayload(id);
|
|
625
848
|
assert.equal(payload.success, true);
|
|
626
|
-
|
|
849
|
+
assertAsyncChildOutput(payload, 0, "settled async response");
|
|
627
850
|
assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during the retry delay");
|
|
628
851
|
});
|
|
629
852
|
|
|
@@ -635,7 +858,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
635
858
|
const payload = await readAsyncPayload(id);
|
|
636
859
|
assert.equal(payload.success, true);
|
|
637
860
|
assert.equal(payload.results[0]?.error, undefined);
|
|
638
|
-
|
|
861
|
+
assertAsyncChildOutput(payload, 0, "settled async without a terminal assistant stop");
|
|
639
862
|
assert.ok(Date.now() - startedAt < 4000, "agent_settled should trigger bounded child cleanup");
|
|
640
863
|
});
|
|
641
864
|
|
|
@@ -666,7 +889,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
666
889
|
assert.match(call.args.at(-1) ?? "", /\{outputs\.name\}/);
|
|
667
890
|
const payload = await readAsyncPayload(id);
|
|
668
891
|
assert.equal(payload.success, true);
|
|
669
|
-
|
|
892
|
+
assertAsyncChildOutput(payload, 0, "OK");
|
|
670
893
|
});
|
|
671
894
|
|
|
672
895
|
it("spawns the async runner with node when process.execPath is not node", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -697,7 +920,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
697
920
|
const resultPath = await waitForAsyncResultFile(id, 30_000);
|
|
698
921
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
699
922
|
assert.equal(payload.success, true);
|
|
700
|
-
|
|
923
|
+
assertAsyncChildOutput(payload, 0, "non-node exec async done");
|
|
701
924
|
} finally {
|
|
702
925
|
process.execPath = originalExecPath;
|
|
703
926
|
}
|
|
@@ -731,7 +954,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
731
954
|
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
732
955
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
733
956
|
assert.equal(payload.success, true);
|
|
734
|
-
|
|
957
|
+
assertAsyncChildOutput(payload, 0, "stale node exec async done");
|
|
735
958
|
} finally {
|
|
736
959
|
process.execPath = originalExecPath;
|
|
737
960
|
}
|
|
@@ -972,8 +1195,11 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
972
1195
|
assert.equal(payload.turnBudget?.turnCount, 2);
|
|
973
1196
|
assert.equal(payload.results[0]?.wrapUpRequested, true);
|
|
974
1197
|
assert.equal(payload.results[0]?.turnBudget?.turnCount, 2);
|
|
975
|
-
|
|
976
|
-
|
|
1198
|
+
// Reference-first delivery: the saved-output reference replaces inline prose;
|
|
1199
|
+
// the wrap-up note and raw output stay visible through status/result fields
|
|
1200
|
+
// and the persisted result file.
|
|
1201
|
+
assert.match(payload.results[0]?.output ?? "", /Output saved to: /);
|
|
1202
|
+
assertAsyncChildOutput(payload, 0, "final wrapped output");
|
|
977
1203
|
assert.equal(status.wrapUpRequested, true);
|
|
978
1204
|
assert.equal(status.turnBudgetExceeded, undefined);
|
|
979
1205
|
assert.equal(status.steps?.[0]?.wrapUpRequested, true);
|
|
@@ -1012,8 +1238,9 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1012
1238
|
assert.equal(payload.turnBudget?.turnCount, 3);
|
|
1013
1239
|
assert.equal(payload.turnBudget?.exceededAtTurn, 3);
|
|
1014
1240
|
assert.equal(payload.results[0]?.turnBudgetExceeded, true);
|
|
1015
|
-
assert.match(payload.
|
|
1016
|
-
assert.match(payload.results[0]?.output ?? "", /
|
|
1241
|
+
assert.match(payload.error ?? "", /Subagent exceeded turn budget|turn budget/i);
|
|
1242
|
+
assert.match(payload.results[0]?.output ?? "", /Output saved to: /);
|
|
1243
|
+
assertAsyncChildOutput(payload, 0, "safe assistant boundary after tool work");
|
|
1017
1244
|
assert.equal(status.state, "failed");
|
|
1018
1245
|
assert.equal(status.turnBudgetExceeded, true);
|
|
1019
1246
|
assert.equal(status.steps?.[0]?.turnBudgetExceeded, true);
|
|
@@ -1066,7 +1293,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1066
1293
|
assert.equal(payload.turnBudget?.outcome, "exceeded");
|
|
1067
1294
|
assert.equal(payload.turnBudget?.turnCount, 2);
|
|
1068
1295
|
assert.equal(payload.results[0]?.turnBudgetExceeded, true);
|
|
1069
|
-
assert.match(payload.results[0]?.output ?? "", /
|
|
1296
|
+
assert.match(payload.results[0]?.output ?? "", /Output saved to: /);
|
|
1297
|
+
assertAsyncChildOutput(payload, 0, "safe assistant boundary reached");
|
|
1070
1298
|
assert.equal(status.state, "failed");
|
|
1071
1299
|
assert.equal(status.turnBudgetExceeded, true);
|
|
1072
1300
|
assert.equal(status.steps?.[0]?.turnBudget?.outcome, "exceeded");
|
|
@@ -1349,15 +1577,16 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1349
1577
|
tempArtifactsDir: tempDir,
|
|
1350
1578
|
getSubagentSessionRoot: () => tempDir,
|
|
1351
1579
|
expandTilde: (p: string) => p,
|
|
1352
|
-
discoverAgents: () => ({ agents: [makeAgent("
|
|
1580
|
+
discoverAgents: () => ({ agents: [makeAgent("builder", { defaultProgress: true })] }),
|
|
1353
1581
|
});
|
|
1354
1582
|
|
|
1355
1583
|
const result = await executor.execute(
|
|
1356
1584
|
"async-parallel-fields",
|
|
1357
1585
|
{
|
|
1358
|
-
tasks: [{ agent: "
|
|
1586
|
+
tasks: [{ agent: "builder", task: "Do async work", output: "async-top-output.md", reads: ["input.md"] }],
|
|
1359
1587
|
async: true,
|
|
1360
1588
|
clarify: false,
|
|
1589
|
+
artifacts: true,
|
|
1361
1590
|
},
|
|
1362
1591
|
new AbortController().signal,
|
|
1363
1592
|
undefined,
|
|
@@ -1416,7 +1645,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1416
1645
|
];
|
|
1417
1646
|
const launch = await executor.execute(
|
|
1418
1647
|
`async-inherited-output-${outputOverride === true ? "true" : "omitted"}`,
|
|
1419
|
-
{ tasks, async: true, clarify: false },
|
|
1648
|
+
{ tasks, async: true, clarify: false, artifacts: true },
|
|
1420
1649
|
new AbortController().signal,
|
|
1421
1650
|
undefined,
|
|
1422
1651
|
makeMinimalCtx(tempDir),
|
|
@@ -1425,8 +1654,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1425
1654
|
assert.equal(launch.isError, undefined);
|
|
1426
1655
|
const payload = await readAsyncPayload(launch.details?.asyncId as string);
|
|
1427
1656
|
assert.equal(payload.success, true);
|
|
1428
|
-
|
|
1429
|
-
|
|
1657
|
+
assertAsyncChildOutput(payload, 0, "first async report");
|
|
1658
|
+
assertAsyncChildOutput(payload, 1, "second async report");
|
|
1430
1659
|
const outputDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", launch.details?.asyncId as string);
|
|
1431
1660
|
const authoritativePaths = [
|
|
1432
1661
|
path.join(outputDir, "parallel-0", "0-worker", "context.md"),
|
|
@@ -1488,6 +1717,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1488
1717
|
],
|
|
1489
1718
|
async: true,
|
|
1490
1719
|
clarify: false,
|
|
1720
|
+
artifacts: true,
|
|
1491
1721
|
},
|
|
1492
1722
|
new AbortController().signal,
|
|
1493
1723
|
undefined,
|
|
@@ -1726,13 +1956,13 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1726
1956
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
1727
1957
|
const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
|
|
1728
1958
|
assert.equal(payload.success, true);
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1959
|
+
// Reference-first chain delivery: every child result carries the saved-output
|
|
1960
|
+
// reference; full text is read from each durable output path.
|
|
1961
|
+
assertAsyncChildOutput(payload, 0, "Scout A async findings");
|
|
1962
|
+
assertAsyncChildOutput(payload, 1, "Scout B async findings");
|
|
1963
|
+
assertAsyncChildOutput(payload, 2, "Async funnel synthesis");
|
|
1964
|
+
assertAsyncChildOutput(payload, 3, "Async reviewer A done");
|
|
1965
|
+
assertAsyncChildOutput(payload, 4, "Async reviewer B done");
|
|
1736
1966
|
assert.deepEqual(status.steps?.map((step) => step.status), ["complete", "complete", "complete", "complete", "complete"]);
|
|
1737
1967
|
assert.deepEqual(status.parallelGroups, [
|
|
1738
1968
|
{ start: 0, count: 2, stepIndex: 0 },
|
|
@@ -1740,11 +1970,14 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1740
1970
|
]);
|
|
1741
1971
|
const funnelTask = readMockPiArgsMatching(mockPi, "Synthesize:").at(-1) ?? "";
|
|
1742
1972
|
assert.match(funnelTask, /=== Parallel Task 1 \(scout-a\) ===/);
|
|
1743
|
-
assert.match(funnelTask, /Scout A async findings/);
|
|
1744
1973
|
assert.match(funnelTask, /=== Parallel Task 2 \(scout-b\) ===/);
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
assert.match(
|
|
1974
|
+
// Chain handoff stays reference-first: the funnel consumes the saved-output
|
|
1975
|
+
// references and reads the named paths instead of re-inlined child prose.
|
|
1976
|
+
assert.match(funnelTask, /Output saved to: /);
|
|
1977
|
+
assert.doesNotMatch(funnelTask, /Scout A async findings/);
|
|
1978
|
+
assert.doesNotMatch(funnelTask, /Scout B async findings/);
|
|
1979
|
+
assert.match(readMockPiArgsMatching(mockPi, "Review funnel A:").at(-1) ?? "", /Review funnel A:\nOutput saved to: /);
|
|
1980
|
+
assert.match(readMockPiArgsMatching(mockPi, "Review funnel B:").at(-1) ?? "", /Review funnel B:\nOutput saved to: /);
|
|
1748
1981
|
assert.equal(payload.workflowGraph?.nodes?.[0]?.kind, "parallel-group");
|
|
1749
1982
|
assert.equal(payload.workflowGraph?.nodes?.[0]?.status, "completed");
|
|
1750
1983
|
assert.equal(payload.workflowGraph?.nodes?.[1]?.kind, "step");
|
|
@@ -1862,6 +2095,39 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1862
2095
|
assert.equal(payload.workflowGraph?.nodes?.[2]?.flatIndex, 3);
|
|
1863
2096
|
});
|
|
1864
2097
|
|
|
2098
|
+
it("async dynamic fanout blocks queued children when hard reported usage is exhausted", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
2099
|
+
mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } });
|
|
2100
|
+
mockPi.onCall({ matchArgIncludes: "Review src/a.ts", output: "review-a", structuredOutput: { ok: "a" } });
|
|
2101
|
+
const id = `async-dynamic-usage-budget-${Date.now().toString(36)}`;
|
|
2102
|
+
executeAsyncChain(id, {
|
|
2103
|
+
chain: [
|
|
2104
|
+
{ agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } },
|
|
2105
|
+
{
|
|
2106
|
+
expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
|
|
2107
|
+
parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } },
|
|
2108
|
+
collect: { as: "reviews" },
|
|
2109
|
+
concurrency: 1,
|
|
2110
|
+
},
|
|
2111
|
+
],
|
|
2112
|
+
usageBudget: { tokens: { hard: 200 } },
|
|
2113
|
+
agents: [makeAgent("producer"), makeAgent("reviewer")],
|
|
2114
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-budget" },
|
|
2115
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
2116
|
+
shareEnabled: false,
|
|
2117
|
+
maxSubagentDepth: 2,
|
|
2118
|
+
});
|
|
2119
|
+
|
|
2120
|
+
const payload = await readAsyncPayload(id);
|
|
2121
|
+
const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
|
|
2122
|
+
assert.equal(mockPi.callCount(), 2);
|
|
2123
|
+
assert.equal(payload.success, false);
|
|
2124
|
+
assert.equal(payload.usageBudget?.exhausted, true);
|
|
2125
|
+
assert.equal(status.steps?.[1]?.status, "complete");
|
|
2126
|
+
assert.equal(status.steps?.[2]?.status, "failed");
|
|
2127
|
+
assert.match(status.steps?.[2]?.error ?? "", /Usage budget exhausted/);
|
|
2128
|
+
assert.equal(payload.results.find((result) => result.agent === "reviewer" && result.skipped)?.skipped, true);
|
|
2129
|
+
});
|
|
2130
|
+
|
|
1865
2131
|
it("rejects a shared explicit output before dynamic fanout children start", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1866
2132
|
mockPi.onCall({ matchArgIncludes: "Produce targets", output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } });
|
|
1867
2133
|
const id = `async-dynamic-explicit-output-${Date.now().toString(36)}`;
|
|
@@ -2102,7 +2368,10 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2102
2368
|
const expectedConsumerTarget = `subagent-consumer-${id}-4`;
|
|
2103
2369
|
assert.equal(payload.success, true);
|
|
2104
2370
|
assert.equal(payload.results[3]?.intercomTarget, expectedConsumerTarget);
|
|
2105
|
-
|
|
2371
|
+
const consumerChild = payload.results[3];
|
|
2372
|
+
assert.ok(consumerChild?.outputPath, "expected a durable output path for the consumer child");
|
|
2373
|
+
assert.match(consumerChild.output ?? "", /Output saved to: /);
|
|
2374
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(consumerChild.outputPath, "utf-8")), { SELESAI_SUBAGENT_INTERCOM_SESSION_NAME: expectedConsumerTarget });
|
|
2106
2375
|
});
|
|
2107
2376
|
|
|
2108
2377
|
it("async dynamic pre-spawn failures persist failed graph status and error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -2189,6 +2458,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2189
2458
|
async: true,
|
|
2190
2459
|
clarify: false,
|
|
2191
2460
|
worktree: true,
|
|
2461
|
+
artifacts: true,
|
|
2192
2462
|
},
|
|
2193
2463
|
new AbortController().signal,
|
|
2194
2464
|
undefined,
|
|
@@ -2360,6 +2630,43 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2360
2630
|
assert.equal(mockPi.callCount(), 2);
|
|
2361
2631
|
});
|
|
2362
2632
|
|
|
2633
|
+
it("background runs retry a zero-activity startup exit on the same model", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
2634
|
+
mockPi.onCall({ exitCode: 1 });
|
|
2635
|
+
mockPi.onCall({ output: "Recovered asynchronously after startup race" });
|
|
2636
|
+
const id = `async-startup-retry-${Date.now().toString(36)}`;
|
|
2637
|
+
executeAsyncSingle(id, {
|
|
2638
|
+
agent: "worker",
|
|
2639
|
+
task: "Do work",
|
|
2640
|
+
agentConfig: makeAgent("worker", {
|
|
2641
|
+
model: "openai/gpt-5-mini:high",
|
|
2642
|
+
fallbackModels: ["anthropic/claude-sonnet-4:low"],
|
|
2643
|
+
}),
|
|
2644
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
2645
|
+
availableModels: [
|
|
2646
|
+
{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
|
|
2647
|
+
{ provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
|
|
2648
|
+
],
|
|
2649
|
+
artifactConfig: {
|
|
2650
|
+
enabled: false,
|
|
2651
|
+
includeInput: false,
|
|
2652
|
+
includeOutput: false,
|
|
2653
|
+
includeJsonl: false,
|
|
2654
|
+
includeMetadata: false,
|
|
2655
|
+
cleanupDays: 7,
|
|
2656
|
+
},
|
|
2657
|
+
shareEnabled: false,
|
|
2658
|
+
maxSubagentDepth: 2,
|
|
2659
|
+
});
|
|
2660
|
+
|
|
2661
|
+
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8")) as AsyncResultPayload;
|
|
2662
|
+
assert.equal(payload.success, true);
|
|
2663
|
+
assert.equal(payload.results[0]?.model, "openai/gpt-5-mini:high");
|
|
2664
|
+
assert.deepEqual(payload.results[0]?.attemptedModels, ["openai/gpt-5-mini:high"]);
|
|
2665
|
+
assert.deepEqual(payload.results[0]?.modelAttempts?.map((attempt) => attempt.success), [false, true]);
|
|
2666
|
+
assertAsyncChildOutput(payload, 0, "Recovered asynchronously after startup race");
|
|
2667
|
+
assert.equal(mockPi.callCount(), 2);
|
|
2668
|
+
});
|
|
2669
|
+
|
|
2363
2670
|
it("background runs retry the fallback model when the provider stream ends without finish_reason", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
2364
2671
|
mockPi.onCall({
|
|
2365
2672
|
jsonl: [{
|
|
@@ -2403,7 +2710,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2403
2710
|
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8"));
|
|
2404
2711
|
assert.equal(payload.success, true);
|
|
2405
2712
|
assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high", "anthropic/claude-sonnet-4:low"]);
|
|
2406
|
-
|
|
2713
|
+
assertAsyncChildOutput(payload, 0, "Recovered after stream failure");
|
|
2407
2714
|
assert.equal(mockPi.callCount(), 2);
|
|
2408
2715
|
});
|
|
2409
2716
|
|
|
@@ -2544,7 +2851,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2544
2851
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
2545
2852
|
assert.equal(payload.success, true);
|
|
2546
2853
|
assert.equal(payload.results[0]?.model, "anthropic/claude-sonnet-4");
|
|
2547
|
-
|
|
2854
|
+
assertAsyncChildOutput(payload, 0, "Recovered asynchronously from empty output");
|
|
2548
2855
|
assert.match(payload.results[0]?.modelAttempts?.[0]?.error ?? "", /no output/i);
|
|
2549
2856
|
assert.deepEqual(payload.results[0]?.modelAttempts?.map((attempt) => attempt.success), [false, true]);
|
|
2550
2857
|
assert.equal(mockPi.callCount(), 2);
|
|
@@ -2684,7 +2991,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
2684
2991
|
assert.equal(payload.exitCode, 0);
|
|
2685
2992
|
assert.equal(payload.results[0]?.success, true);
|
|
2686
2993
|
assert.equal(payload.results[0]?.error, undefined);
|
|
2687
|
-
|
|
2994
|
+
assertAsyncChildOutput(payload, 0, "Recovered asynchronously");
|
|
2688
2995
|
const statusPayload = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload;
|
|
2689
2996
|
assert.equal(statusPayload.state, "complete");
|
|
2690
2997
|
assert.equal(statusPayload.steps?.[0]?.status, "complete");
|
|
@@ -3088,7 +3395,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3088
3395
|
assert.equal(payload.success, true);
|
|
3089
3396
|
assert.equal(payload.exitCode, 0);
|
|
3090
3397
|
assert.equal(payload.results[0].success, true);
|
|
3091
|
-
|
|
3398
|
+
assertAsyncChildOutput(payload, 0, "cold start test after patch");
|
|
3092
3399
|
|
|
3093
3400
|
const eventsPath = path.join(ASYNC_DIR, id, "events.jsonl");
|
|
3094
3401
|
const eventsText = fs.readFileSync(eventsPath, "utf-8");
|
|
@@ -3143,6 +3450,71 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3143
3450
|
assert.deepEqual(payload.results[0].attemptedModels, ["github-copilot/gpt-5-mini"]);
|
|
3144
3451
|
});
|
|
3145
3452
|
|
|
3453
|
+
it("async executor keeps the last parent session model after continuation drops ctx.model", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
3454
|
+
mockPi.onCall({ output: "Done asynchronously" });
|
|
3455
|
+
const state = {
|
|
3456
|
+
baseCwd: tempDir,
|
|
3457
|
+
currentSessionId: null,
|
|
3458
|
+
asyncJobs: new Map(),
|
|
3459
|
+
foregroundControls: new Map(),
|
|
3460
|
+
lastForegroundControlId: null,
|
|
3461
|
+
};
|
|
3462
|
+
const executor = createSubagentExecutor!({
|
|
3463
|
+
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
3464
|
+
state,
|
|
3465
|
+
config: {},
|
|
3466
|
+
asyncByDefault: false,
|
|
3467
|
+
tempArtifactsDir: tempDir,
|
|
3468
|
+
getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
|
|
3469
|
+
expandTilde: (p: string) => p,
|
|
3470
|
+
discoverAgents: () => ({ agents: [makeAgent("worker")] }),
|
|
3471
|
+
});
|
|
3472
|
+
const initialCtx = makeMinimalCtx(tempDir);
|
|
3473
|
+
initialCtx.sessionManager.getSessionId = () => "session-continued";
|
|
3474
|
+
initialCtx.model = { provider: "deepseek", id: "deepseek-v4-flash" };
|
|
3475
|
+
await executor.execute("prime-parent-model", { action: "list" }, new AbortController().signal, undefined, initialCtx);
|
|
3476
|
+
|
|
3477
|
+
const continuedCtx = makeMinimalCtx(tempDir);
|
|
3478
|
+
continuedCtx.sessionManager.getSessionId = () => "session-continued";
|
|
3479
|
+
const launch = await executor.execute(
|
|
3480
|
+
"continued-async-child",
|
|
3481
|
+
{ agent: "worker", task: "Do work", async: true, acceptance: false },
|
|
3482
|
+
new AbortController().signal,
|
|
3483
|
+
undefined,
|
|
3484
|
+
continuedCtx,
|
|
3485
|
+
) as AsyncExecutionResult;
|
|
3486
|
+
assert.equal(launch.isError, undefined);
|
|
3487
|
+
assert.ok(launch.details.asyncId);
|
|
3488
|
+
|
|
3489
|
+
const payload = await readAsyncPayload(launch.details.asyncId);
|
|
3490
|
+
assert.equal(payload.success, true);
|
|
3491
|
+
assert.equal(payload.results[0]?.model, "deepseek/deepseek-v4-flash");
|
|
3492
|
+
const args = readMockPiArgs(mockPi, 0);
|
|
3493
|
+
assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
|
|
3494
|
+
});
|
|
3495
|
+
|
|
3496
|
+
it("foreground chains keep the last parent session model after continuation drops ctx.model", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
|
|
3497
|
+
mockPi.onCall({ output: "Done foreground" });
|
|
3498
|
+
const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
|
|
3499
|
+
const initialCtx = makeMinimalCtx(tempDir);
|
|
3500
|
+
initialCtx.sessionManager.getSessionId = () => "session-continued";
|
|
3501
|
+
initialCtx.model = { provider: "deepseek", id: "deepseek-v4-flash" };
|
|
3502
|
+
await executor.execute("prime-parent-model", { action: "list" }, new AbortController().signal, undefined, initialCtx);
|
|
3503
|
+
|
|
3504
|
+
const continuedCtx = makeMinimalCtx(tempDir);
|
|
3505
|
+
continuedCtx.sessionManager.getSessionId = () => "session-continued";
|
|
3506
|
+
const result = await executor.execute(
|
|
3507
|
+
"continued-foreground-chain",
|
|
3508
|
+
{ chain: [{ agent: "worker", task: "Do work" }], acceptance: false },
|
|
3509
|
+
new AbortController().signal,
|
|
3510
|
+
undefined,
|
|
3511
|
+
continuedCtx,
|
|
3512
|
+
);
|
|
3513
|
+
assert.equal(result.isError, undefined);
|
|
3514
|
+
const args = readMockPiArgs(mockPi, 0);
|
|
3515
|
+
assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3146
3518
|
it("background single runs inherit the parent session model when no model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
3147
3519
|
mockPi.onCall({ output: "Done asynchronously" });
|
|
3148
3520
|
|
|
@@ -3729,7 +4101,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3729
4101
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
3730
4102
|
assert.ok(elapsed < 6000, `unconfigured watchdog status should not delay async final drain, took ${elapsed}ms`);
|
|
3731
4103
|
assert.equal(payload.success, true);
|
|
3732
|
-
|
|
4104
|
+
assertAsyncChildOutput(payload, 0, "async-done-without-watchdog-config");
|
|
3733
4105
|
assert.equal((payload.results[0] as { watchdog?: unknown }).watchdog, undefined);
|
|
3734
4106
|
});
|
|
3735
4107
|
});
|
|
@@ -3764,7 +4136,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3764
4136
|
assert.ok(elapsed >= 1200, `watchdog settlement should delay async final drain, took ${elapsed}ms`);
|
|
3765
4137
|
assert.ok(elapsed < 9000, `settled watchdog should still allow async cleanup, took ${elapsed}ms`);
|
|
3766
4138
|
assert.equal(payload.success, true);
|
|
3767
|
-
|
|
4139
|
+
assertAsyncChildOutput(payload, 0, "async-done-before-watchdog");
|
|
3768
4140
|
assert.equal((payload.results[0] as { watchdog?: { phase?: string } }).watchdog?.phase, "idle");
|
|
3769
4141
|
});
|
|
3770
4142
|
});
|
|
@@ -3795,7 +4167,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3795
4167
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
3796
4168
|
assert.ok(elapsed < 6000, `watchdog tail fallback should not hang async final drain, took ${elapsed}ms`);
|
|
3797
4169
|
assert.equal(payload.success, true);
|
|
3798
|
-
|
|
4170
|
+
assertAsyncChildOutput(payload, 0, "async-done-before-watchdog-timeout");
|
|
3799
4171
|
const watchdog = (payload.results[0] as { watchdog?: { phase?: string; timedOut?: boolean } }).watchdog;
|
|
3800
4172
|
assert.equal(watchdog?.phase, "stale");
|
|
3801
4173
|
assert.equal(watchdog?.timedOut, true);
|
|
@@ -3846,7 +4218,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3846
4218
|
assert.equal(payload.success, true);
|
|
3847
4219
|
assert.equal(payload.exitCode, 0);
|
|
3848
4220
|
assert.equal(payload.results[0].success, true);
|
|
3849
|
-
|
|
4221
|
+
assertAsyncChildOutput(payload, 0, "async-done-before-drain");
|
|
3850
4222
|
});
|
|
3851
4223
|
|
|
3852
4224
|
it("background forced drain after empty terminal assistant output is cleanup success", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -3882,7 +4254,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3882
4254
|
assert.equal(payload.success, true);
|
|
3883
4255
|
assert.equal(payload.exitCode, 0);
|
|
3884
4256
|
assert.equal(payload.results[0].success, true);
|
|
3885
|
-
assert.
|
|
4257
|
+
assert.match(payload.results[0].output ?? "", /Output saved to: /);
|
|
3886
4258
|
});
|
|
3887
4259
|
|
|
3888
4260
|
it("background final-drain cleanup preserves explicit assistant errors", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
@@ -4255,7 +4627,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4255
4627
|
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
4256
4628
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
|
|
4257
4629
|
assert.equal(payload.success, true);
|
|
4258
|
-
|
|
4630
|
+
assertAsyncChildOutput(payload, 0, "Done after noisy stream");
|
|
4259
4631
|
|
|
4260
4632
|
const eventsText = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8");
|
|
4261
4633
|
assert.doesNotMatch(eventsText, /"type":"message_update"/);
|
|
@@ -4334,7 +4706,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4334
4706
|
|
|
4335
4707
|
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
4336
4708
|
assert.equal(payload.success, true);
|
|
4337
|
-
|
|
4709
|
+
assertAsyncChildOutput(payload, 0, "Done streaming");
|
|
4338
4710
|
|
|
4339
4711
|
const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8"));
|
|
4340
4712
|
assert.deepEqual(status.steps[0].recentTools.map((tool: { tool: string; args: string }) => ({ tool: tool.tool, args: tool.args })), [{ tool: "bash", args: "ls" }]);
|