@selesai/code 0.5.19 → 0.5.21
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/extensions/handoff-new.test.ts +8 -9
- package/dist/extensions/handoff-new.ts +6 -8
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +31 -0
- package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
- package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
- package/dist/extensions/pi-subagents/README.md +602 -220
- package/dist/extensions/pi-subagents/banner.png +0 -0
- package/dist/extensions/pi-subagents/index.ts +1 -0
- package/dist/extensions/pi-subagents/install.mjs +3 -3
- package/dist/extensions/pi-subagents/package-lock.json +3405 -0
- package/dist/extensions/pi-subagents/package.json +26 -14
- package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
- package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
- package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
- package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
- package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
- package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
- package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
- package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
- package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
- package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
- package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
- package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
- package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
- package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
- package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
- package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
- package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
- package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
- package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
- package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
- package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
- package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
- package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
- package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
- package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
- package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
- package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
- package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
- package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
- package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
- package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
- package/dist/extensions/question/batch.ts +39 -0
- package/dist/extensions/question/dialog-adapter.ts +23 -1
- package/dist/extensions/question/index.ts +237 -7
- package/dist/extensions/question/schemas.ts +28 -15
- package/dist/extensions/question/tests/batch.test.ts +26 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
- package/dist/extensions/question/tui-adapter.ts +2 -3
- package/dist/extensions/question/types.ts +37 -0
- package/dist/extensions/question/ui-protocol.ts +3 -3
- package/dist/skills/batch-grill-me/SKILL.md +3 -1
- package/package.json +2 -1
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
SUBAGENT_DELEGATION_CANCEL_EVENT,
|
|
6
|
+
SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
7
|
+
SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
|
|
8
|
+
SUBAGENT_DELEGATION_REQUEST_EVENT,
|
|
9
|
+
SUBAGENT_DELEGATION_RESPONSE_EVENT,
|
|
10
|
+
SUBAGENT_DELEGATION_STARTED_EVENT,
|
|
11
|
+
SUBAGENT_DELEGATION_UPDATE_EVENT,
|
|
12
|
+
type SubagentDelegationAcceptance,
|
|
13
|
+
type SubagentDelegationRequest,
|
|
14
|
+
type SubagentDelegationResponse,
|
|
15
|
+
type SubagentDelegationV2InvalidResponse,
|
|
16
|
+
type SubagentDelegationV2Request,
|
|
17
|
+
type SubagentDelegationV2Response,
|
|
18
|
+
} from "../../src/api/delegation.ts";
|
|
19
|
+
import { parseSubagentDelegationRequest } from "../../src/slash/delegation-request.ts";
|
|
20
|
+
import {
|
|
21
|
+
registerPromptTemplateDelegationBridge,
|
|
22
|
+
type PromptTemplateBridgeEvents,
|
|
23
|
+
} from "../../src/slash/prompt-template-bridge.ts";
|
|
24
|
+
|
|
25
|
+
class FakeEvents implements PromptTemplateBridgeEvents {
|
|
26
|
+
private handlers = new Map<string, Array<(data: unknown) => void>>();
|
|
27
|
+
|
|
28
|
+
on(event: string, handler: (data: unknown) => void): () => void {
|
|
29
|
+
const handlers = this.handlers.get(event) ?? [];
|
|
30
|
+
handlers.push(handler);
|
|
31
|
+
this.handlers.set(event, handlers);
|
|
32
|
+
return () => this.handlers.set(event, (this.handlers.get(event) ?? []).filter((entry) => entry !== handler));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
emit(event: string, data: unknown): void {
|
|
36
|
+
for (const handler of [...(this.handlers.get(event) ?? [])]) handler(data);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function once(events: FakeEvents, event: string): Promise<unknown> {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const unsubscribe = events.on(event, (payload) => {
|
|
43
|
+
unsubscribe();
|
|
44
|
+
resolve(payload);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tick(): Promise<void> {
|
|
50
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const acceptance: SubagentDelegationAcceptance = {
|
|
54
|
+
level: "verified",
|
|
55
|
+
criteria: [{ id: "criterion-1", must: "Verify the result", evidence: ["validation-output"], severity: "required" }],
|
|
56
|
+
evidence: ["commands-run", "validation-output"],
|
|
57
|
+
verify: [{ id: "test", command: "npm test", timeoutMs: 1_000, cwd: "/repo", env: { CI: "true" }, allowFailure: false }],
|
|
58
|
+
review: { agent: "reviewer", focus: "correctness", required: false },
|
|
59
|
+
stopRules: ["Stop on verification failure"],
|
|
60
|
+
reason: "Explicit verification contract",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const v2Request: SubagentDelegationV2Request = {
|
|
64
|
+
version: 2,
|
|
65
|
+
requestId: "attempt-1",
|
|
66
|
+
ownerRunId: "owner-1",
|
|
67
|
+
nodeId: "node-1",
|
|
68
|
+
agent: "reviewer",
|
|
69
|
+
task: "Review evidence",
|
|
70
|
+
context: "fresh",
|
|
71
|
+
cwd: "/repo",
|
|
72
|
+
model: "openai/gpt-5",
|
|
73
|
+
thinking: "high",
|
|
74
|
+
timeoutMs: 1_000,
|
|
75
|
+
turnBudget: { maxTurns: 4, graceTurns: 1 },
|
|
76
|
+
toolBudget: { soft: 3, hard: 5, block: "*" },
|
|
77
|
+
skill: ["review"],
|
|
78
|
+
artifacts: true,
|
|
79
|
+
result: { kind: "structured", schema: { type: "object", properties: { ok: { type: "boolean" } } } },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const request: SubagentDelegationRequest = {
|
|
83
|
+
version: 1,
|
|
84
|
+
requestId: "delegation-1",
|
|
85
|
+
agent: "reviewer",
|
|
86
|
+
task: "Review evidence",
|
|
87
|
+
context: "fresh",
|
|
88
|
+
cwd: "/repo",
|
|
89
|
+
model: "openai/gpt-5",
|
|
90
|
+
timeoutMs: 1_000,
|
|
91
|
+
turnBudget: { maxTurns: 4, graceTurns: 1 },
|
|
92
|
+
toolBudget: { soft: 3, hard: 5, block: "*" },
|
|
93
|
+
skill: ["review"],
|
|
94
|
+
output: "result.md",
|
|
95
|
+
outputMode: "file-only",
|
|
96
|
+
outputSchema: { type: "object", properties: { ok: { type: "boolean" } } },
|
|
97
|
+
agentContract: { version: 1 },
|
|
98
|
+
acceptance: "checked",
|
|
99
|
+
artifacts: true,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
describe("public subagent delegation contract", () => {
|
|
103
|
+
it("uses the existing prompt-template event family as the only transport", () => {
|
|
104
|
+
assert.equal(SUBAGENT_DELEGATION_PROTOCOL_VERSION, 1);
|
|
105
|
+
assert.equal(SUBAGENT_DELEGATION_REQUEST_EVENT, "prompt-template:subagent:request");
|
|
106
|
+
assert.equal(SUBAGENT_DELEGATION_STARTED_EVENT, "prompt-template:subagent:started");
|
|
107
|
+
assert.equal(SUBAGENT_DELEGATION_UPDATE_EVENT, "prompt-template:subagent:update");
|
|
108
|
+
assert.equal(SUBAGENT_DELEGATION_RESPONSE_EVENT, "prompt-template:subagent:response");
|
|
109
|
+
assert.equal(SUBAGENT_DELEGATION_CANCEL_EVENT, "prompt-template:subagent:cancel");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("strictly parses the complete v2 request and enforces its independent allowlists and bounds", () => {
|
|
113
|
+
assert.equal(SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION, 2);
|
|
114
|
+
assert.deepEqual(parseSubagentDelegationRequest(v2Request), { ok: true, request: v2Request });
|
|
115
|
+
const malformed = [
|
|
116
|
+
[{ ...v2Request, ownerRunId: "bad\nowner" }, /ownerRunId.*256 characters without newlines/],
|
|
117
|
+
[{ ...v2Request, nodeId: "x".repeat(257) }, /nodeId.*256 characters without newlines/],
|
|
118
|
+
[{ ...v2Request, thinking: "extreme" }, /thinking must be one of/],
|
|
119
|
+
[{ ...v2Request, output: false }, /Unsupported delegation field: output/],
|
|
120
|
+
[{ ...v2Request, outputMode: "inline" }, /Unsupported delegation field: outputMode/],
|
|
121
|
+
[{ ...v2Request, agentContract: { version: 1 } }, /Unsupported delegation field: agentContract/],
|
|
122
|
+
[{ ...v2Request, acceptance: false }, /Unsupported delegation field: acceptance/],
|
|
123
|
+
[{ ...v2Request, result: { kind: "text", schema: {} } }, /result.schema is not supported/],
|
|
124
|
+
[{ ...v2Request, result: { kind: "structured" } }, /result.schema must be a JSON Schema object/],
|
|
125
|
+
[{ ...v2Request, task: "é".repeat(524_289) }, /task exceeds 1 MiB/],
|
|
126
|
+
[{ ...v2Request, cwd: "é".repeat(16_385) }, /cwd exceeds 32 KiB/],
|
|
127
|
+
[{ ...v2Request, agent: "é".repeat(513) }, /agent exceeds 1 KiB/],
|
|
128
|
+
[{ ...v2Request, model: "é".repeat(513) }, /model exceeds 1 KiB/],
|
|
129
|
+
[{ ...v2Request, skill: "é".repeat(513) }, /skill entry exceeds 1 KiB/],
|
|
130
|
+
[{ ...v2Request, skill: Array.from({ length: 257 }, () => "x") }, /skill supports at most 256 entries/],
|
|
131
|
+
[{ ...v2Request, skill: Array.from({ length: 256 }, () => "x".repeat(257)) }, /skill entries exceed 64 KiB in aggregate/],
|
|
132
|
+
[{ ...v2Request, result: { kind: "structured", schema: { value: "x".repeat(65_536) } } }, /result.schema exceeds 64 KiB/],
|
|
133
|
+
[{ ...v2Request, timeoutMs: 2_147_483_648 }, /timeoutMs must be <= 2147483647 for delegation v2/],
|
|
134
|
+
] as const;
|
|
135
|
+
for (const [input, expected] of malformed) {
|
|
136
|
+
const parsed = parseSubagentDelegationRequest(input);
|
|
137
|
+
assert.equal(parsed.ok, false);
|
|
138
|
+
if (!parsed.ok) assert.match(parsed.error, expected);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("accepts an exact zero tool budget only for v2", () => {
|
|
143
|
+
const zeroBudget = { hard: 0, block: "*" as const };
|
|
144
|
+
const parsedV2 = parseSubagentDelegationRequest({ ...v2Request, toolBudget: zeroBudget });
|
|
145
|
+
assert.equal(parsedV2.ok, true);
|
|
146
|
+
if (parsedV2.ok) assert.deepEqual(parsedV2.request.toolBudget, zeroBudget);
|
|
147
|
+
|
|
148
|
+
const parsedV1 = parseSubagentDelegationRequest({ ...request, toolBudget: zeroBudget });
|
|
149
|
+
assert.equal(parsedV1.ok, false);
|
|
150
|
+
if (!parsedV1.ok) assert.equal(parsedV1.error, "toolBudget.hard must be an integer >= 1.");
|
|
151
|
+
for (const soft of [0, 1]) {
|
|
152
|
+
assert.equal(parseSubagentDelegationRequest({ ...v2Request, toolBudget: { ...zeroBudget, soft } }).ok, false);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("rejects non-JSON v2 schemas without executing toJSON hooks", () => {
|
|
157
|
+
let calls = 0;
|
|
158
|
+
const parsed = parseSubagentDelegationRequest({
|
|
159
|
+
...v2Request,
|
|
160
|
+
result: {
|
|
161
|
+
kind: "structured",
|
|
162
|
+
schema: { toJSON: () => { calls++; return {}; } },
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
assert.equal(parsed.ok, false);
|
|
166
|
+
if (!parsed.ok) assert.match(parsed.error, /result.schema must be plain JSON data/);
|
|
167
|
+
assert.equal(calls, 0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("strictly parses the complete v1 request", () => {
|
|
171
|
+
assert.deepEqual(parseSubagentDelegationRequest(request), { ok: true, request });
|
|
172
|
+
const requestWithAcceptance = { ...request, acceptance };
|
|
173
|
+
assert.deepEqual(parseSubagentDelegationRequest(requestWithAcceptance), { ok: true, request: requestWithAcceptance });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("rejects unsupported versions, unknown fields, aliases, and malformed controls", () => {
|
|
177
|
+
const malformed = [
|
|
178
|
+
[{ ...request, version: 2 }, /Unsupported delegation protocol version/],
|
|
179
|
+
[{ ...request, tools: ["write"] }, /Unsupported delegation field: tools/],
|
|
180
|
+
[{ ...request, maxRuntimeMs: 1_000 }, /Unsupported delegation field: maxRuntimeMs/],
|
|
181
|
+
[{ ...request, timeoutMs: 0 }, /timeoutMs must be an integer >= 1/],
|
|
182
|
+
[{ ...request, turnBudget: { maxTurns: 0 } }, /turnBudget.maxTurns/],
|
|
183
|
+
[{ ...request, turnBudget: { maxTurns: 1, extra: true } }, /turnBudget.extra is not supported/],
|
|
184
|
+
[{ ...request, toolBudget: { hard: 1, soft: 2 } }, /toolBudget.soft must be <=/],
|
|
185
|
+
[{ ...request, toolBudget: { hard: 1, extra: true } }, /toolBudget.extra is not supported/],
|
|
186
|
+
[{ ...request, skill: [] }, /skill must/],
|
|
187
|
+
[{ ...request, output: "" }, /output must/],
|
|
188
|
+
[{ ...request, output: false, outputMode: "file-only" }, /outputMode.*output.*path/],
|
|
189
|
+
[{ ...request, outputSchema: [] }, /outputSchema must be a JSON Schema object/],
|
|
190
|
+
[{ ...request, agentContract: { version: 2 } }, /agentContract must be \{ version: 1 \}/],
|
|
191
|
+
[{ ...request, agentContract: { version: 1, extra: true } }, /agentContract must be \{ version: 1 \}/],
|
|
192
|
+
[{ ...request, acceptance: "none" }, /level "none" requires a reason/],
|
|
193
|
+
[{ ...request, acceptance: { level: "none" } }, /reason is required/],
|
|
194
|
+
[{ ...request, artifacts: "yes" }, /artifacts must be a boolean/],
|
|
195
|
+
] as const;
|
|
196
|
+
for (const [input, expected] of malformed) {
|
|
197
|
+
const parsed = parseSubagentDelegationRequest(input);
|
|
198
|
+
assert.equal(parsed.ok, false);
|
|
199
|
+
if (!parsed.ok) assert.match(parsed.error, expected);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("runs v2 through the versioned executor and preserves literal text with full effective metadata", async () => {
|
|
204
|
+
const events = new FakeEvents();
|
|
205
|
+
let ordinaryCalls = 0;
|
|
206
|
+
let observedParams: Record<string, unknown> | undefined;
|
|
207
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
208
|
+
events,
|
|
209
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
210
|
+
execute: async () => {
|
|
211
|
+
ordinaryCalls++;
|
|
212
|
+
return { details: { mode: "single", results: [] } };
|
|
213
|
+
},
|
|
214
|
+
executeVersioned: async (_id, params, _signal, _ctx, onUpdate) => {
|
|
215
|
+
observedParams = params as unknown as Record<string, unknown>;
|
|
216
|
+
onUpdate({ details: { mode: "single", results: [{ agent: "reviewer", model: "openai/gpt-5", thinking: "high" }], progress: [{ currentTool: "read" }] } });
|
|
217
|
+
return {
|
|
218
|
+
details: {
|
|
219
|
+
mode: "single",
|
|
220
|
+
runId: "run-v2",
|
|
221
|
+
results: [{
|
|
222
|
+
agent: "reviewer",
|
|
223
|
+
exitCode: 0,
|
|
224
|
+
model: "openai/gpt-5",
|
|
225
|
+
thinking: "high",
|
|
226
|
+
finalOutput: '{"looks":"json"}',
|
|
227
|
+
usage: { input: 2, output: 3, cacheRead: 4, cacheWrite: 5, cost: 0.01, turns: 2 },
|
|
228
|
+
progressSummary: { toolCount: 6, tokens: 5, durationMs: 7 },
|
|
229
|
+
}],
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
const textRequest = { ...v2Request, result: { kind: "text" as const } };
|
|
235
|
+
const startedPromise = once(events, SUBAGENT_DELEGATION_STARTED_EVENT);
|
|
236
|
+
const updatePromise = once(events, SUBAGENT_DELEGATION_UPDATE_EVENT);
|
|
237
|
+
const responsePromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
238
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, textRequest);
|
|
239
|
+
assert.deepEqual(await startedPromise, { version: 2, requestId: "attempt-1", ownerRunId: "owner-1", nodeId: "node-1" });
|
|
240
|
+
assert.deepEqual(await updatePromise, { version: 2, requestId: "attempt-1", ownerRunId: "owner-1", nodeId: "node-1", currentTool: "read", model: "openai/gpt-5" });
|
|
241
|
+
assert.deepEqual(await responsePromise, {
|
|
242
|
+
version: 2,
|
|
243
|
+
requestId: "attempt-1",
|
|
244
|
+
ownerRunId: "owner-1",
|
|
245
|
+
nodeId: "node-1",
|
|
246
|
+
status: "completed",
|
|
247
|
+
runId: "run-v2",
|
|
248
|
+
agent: "reviewer",
|
|
249
|
+
model: "openai/gpt-5",
|
|
250
|
+
thinking: "high",
|
|
251
|
+
exitCode: 0,
|
|
252
|
+
result: { kind: "text", text: '{"looks":"json"}' },
|
|
253
|
+
usage: { input: 2, output: 3, cacheRead: 4, cacheWrite: 5, cost: 0.01, turns: 2, toolCalls: 6, durationMs: 7 },
|
|
254
|
+
} satisfies SubagentDelegationV2Response);
|
|
255
|
+
assert.equal(ordinaryCalls, 0);
|
|
256
|
+
assert.deepEqual(observedParams, {
|
|
257
|
+
agent: "reviewer",
|
|
258
|
+
task: "Review evidence",
|
|
259
|
+
context: "fresh",
|
|
260
|
+
cwd: "/repo",
|
|
261
|
+
model: "openai/gpt-5",
|
|
262
|
+
timeoutMs: 1_000,
|
|
263
|
+
turnBudget: { maxTurns: 4, graceTurns: 1 },
|
|
264
|
+
enforceHardTurnLimit: true,
|
|
265
|
+
toolBudget: { soft: 3, hard: 5, block: "*" },
|
|
266
|
+
skill: ["review"],
|
|
267
|
+
output: false,
|
|
268
|
+
acceptance: false,
|
|
269
|
+
artifacts: true,
|
|
270
|
+
delegatedThinkingOverride: "high",
|
|
271
|
+
delegatedAllowZeroToolBudget: true,
|
|
272
|
+
async: false,
|
|
273
|
+
foregroundOnly: true,
|
|
274
|
+
clarify: false,
|
|
275
|
+
});
|
|
276
|
+
bridge.dispose();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("projects structured v2 values and fails missing or oversized captures", async () => {
|
|
280
|
+
const cases = [
|
|
281
|
+
[{ ok: true }, "completed", { kind: "structured", value: { ok: true } }],
|
|
282
|
+
[undefined, "failed", undefined],
|
|
283
|
+
[{ value: "x".repeat(1024 * 1024) }, "failed", undefined],
|
|
284
|
+
] as const;
|
|
285
|
+
for (const [structuredOutput, expectedStatus, expectedResult] of cases) {
|
|
286
|
+
const events = new FakeEvents();
|
|
287
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
288
|
+
events,
|
|
289
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
290
|
+
execute: async () => { throw new Error("ordinary executor must remain separate"); },
|
|
291
|
+
executeVersioned: async () => ({
|
|
292
|
+
details: {
|
|
293
|
+
mode: "single",
|
|
294
|
+
results: [{
|
|
295
|
+
agent: "reviewer",
|
|
296
|
+
exitCode: 0,
|
|
297
|
+
...(structuredOutput === undefined ? {} : { structuredOutput }),
|
|
298
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 1 },
|
|
299
|
+
}],
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
const responsePromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
304
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: `structured-${expectedStatus}-${Math.random()}` });
|
|
305
|
+
const response = await responsePromise as SubagentDelegationV2Response;
|
|
306
|
+
assert.equal(response.status, expectedStatus);
|
|
307
|
+
assert.deepEqual(response.result, expectedResult);
|
|
308
|
+
if (expectedStatus === "failed") assert.match(response.error ?? "", /structured result/);
|
|
309
|
+
bridge.dispose();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("rejects non-JSON structured results without executing toJSON hooks", async () => {
|
|
314
|
+
let calls = 0;
|
|
315
|
+
const events = new FakeEvents();
|
|
316
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
317
|
+
events,
|
|
318
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
319
|
+
execute: async () => { throw new Error("ordinary executor must remain separate"); },
|
|
320
|
+
executeVersioned: async () => ({
|
|
321
|
+
details: {
|
|
322
|
+
mode: "single",
|
|
323
|
+
results: [{
|
|
324
|
+
agent: "reviewer",
|
|
325
|
+
exitCode: 0,
|
|
326
|
+
structuredOutput: { toJSON: () => { calls++; return {}; } },
|
|
327
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 1 },
|
|
328
|
+
}],
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
const responsePromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
333
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: "unsafe-structured" });
|
|
334
|
+
const response = await responsePromise as SubagentDelegationV2Response;
|
|
335
|
+
assert.equal(response.status, "failed");
|
|
336
|
+
assert.match(response.error ?? "", /structured result is not plain JSON data/);
|
|
337
|
+
assert.equal(calls, 0);
|
|
338
|
+
bridge.dispose();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("rejects v2 text results exceeding 1 MiB when UTF-8 encoded", async () => {
|
|
342
|
+
const events = new FakeEvents();
|
|
343
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
344
|
+
events,
|
|
345
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
346
|
+
execute: async () => { throw new Error("ordinary executor must remain separate"); },
|
|
347
|
+
executeVersioned: async () => ({
|
|
348
|
+
details: { mode: "single", results: [{ agent: "reviewer", exitCode: 0, finalOutput: "é".repeat(524_289) }] },
|
|
349
|
+
}),
|
|
350
|
+
});
|
|
351
|
+
const responsePromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
352
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: "oversized-text", result: { kind: "text" } });
|
|
353
|
+
const response = await responsePromise as SubagentDelegationV2Response;
|
|
354
|
+
assert.equal(response.status, "failed");
|
|
355
|
+
assert.match(response.error ?? "", /text result exceeds 1 MiB/);
|
|
356
|
+
assert.equal("result" in response, false);
|
|
357
|
+
bridge.dispose();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("isolates v2 logical-node ownership, exact cancellation, pre-cancellation, and reuse", async () => {
|
|
361
|
+
const events = new FakeEvents();
|
|
362
|
+
const releases = new Map<string, () => void>();
|
|
363
|
+
const responses: SubagentDelegationV2Response[] = [];
|
|
364
|
+
events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => responses.push(payload as SubagentDelegationV2Response));
|
|
365
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
366
|
+
events,
|
|
367
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
368
|
+
execute: async () => { throw new Error("ordinary executor must remain separate"); },
|
|
369
|
+
executeVersioned: async (id, params, signal) => await new Promise((resolve, reject) => {
|
|
370
|
+
releases.set(id, () => resolve({ details: { mode: "single", results: [{ agent: params.agent, exitCode: 0, finalOutput: id, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 1 } }] } }));
|
|
371
|
+
signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
372
|
+
}),
|
|
373
|
+
});
|
|
374
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: "owner-a", result: { kind: "text" } });
|
|
375
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: "owner-b", result: { kind: "text" } });
|
|
376
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: "owner-b", result: { kind: "text" } });
|
|
377
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: "other-node", nodeId: "node-2", result: { kind: "text" } });
|
|
378
|
+
while (!releases.has("owner-a") || !releases.has("other-node")) await tick();
|
|
379
|
+
await tick();
|
|
380
|
+
assert.equal(responses.find((entry) => entry.requestId === "owner-b")?.status, "duplicate_node");
|
|
381
|
+
assert.equal(responses.filter((entry) => entry.requestId === "owner-b").length, 1);
|
|
382
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 2, requestId: "owner-a", ownerRunId: "wrong", nodeId: "node-1" });
|
|
383
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 2, requestId: "owner-a", ownerRunId: "owner-1", nodeId: "wrong" });
|
|
384
|
+
await tick();
|
|
385
|
+
assert.equal(responses.some((entry) => entry.requestId === "owner-a"), false);
|
|
386
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 2, requestId: "owner-a", ownerRunId: "owner-1", nodeId: "node-1" });
|
|
387
|
+
while (!responses.some((entry) => entry.requestId === "owner-a")) await tick();
|
|
388
|
+
assert.equal(responses.find((entry) => entry.requestId === "owner-a")?.status, "cancelled");
|
|
389
|
+
releases.get("other-node")?.();
|
|
390
|
+
while (!responses.some((entry) => entry.requestId === "other-node")) await tick();
|
|
391
|
+
|
|
392
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 2, requestId: "pre", ownerRunId: "owner-1", nodeId: "node-1" });
|
|
393
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: "pre", result: { kind: "text" } });
|
|
394
|
+
while (!responses.some((entry) => entry.requestId === "pre")) await tick();
|
|
395
|
+
assert.equal(responses.find((entry) => entry.requestId === "pre")?.status, "cancelled");
|
|
396
|
+
|
|
397
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: "reuse", result: { kind: "text" } });
|
|
398
|
+
while (!releases.has("reuse")) await tick();
|
|
399
|
+
releases.get("reuse")?.();
|
|
400
|
+
while (!responses.some((entry) => entry.requestId === "reuse")) await tick();
|
|
401
|
+
assert.equal(responses.find((entry) => entry.requestId === "reuse")?.status, "completed");
|
|
402
|
+
bridge.dispose();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("keys concurrent v2 attempts and retransmissions by the full identity tuple", async () => {
|
|
406
|
+
const events = new FakeEvents();
|
|
407
|
+
const releases = new Map<string, () => void>();
|
|
408
|
+
const responses: SubagentDelegationV2Response[] = [];
|
|
409
|
+
let executeCalls = 0;
|
|
410
|
+
events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => responses.push(payload as SubagentDelegationV2Response));
|
|
411
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
412
|
+
events,
|
|
413
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
414
|
+
execute: async () => { throw new Error("ordinary executor must remain separate"); },
|
|
415
|
+
executeVersioned: async (_id, params) => {
|
|
416
|
+
executeCalls++;
|
|
417
|
+
if (typeof params.task !== "string") throw new Error("expected a single delegated task");
|
|
418
|
+
const task = params.task;
|
|
419
|
+
await new Promise<void>((resolve) => releases.set(task, resolve));
|
|
420
|
+
return { details: { mode: "single", results: [{ agent: params.agent, exitCode: 0, finalOutput: task }] } };
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
const first = { ...v2Request, requestId: "shared-attempt", task: "first", result: { kind: "text" as const } };
|
|
424
|
+
const second = { ...first, ownerRunId: "owner-2", nodeId: "node-2", task: "second" };
|
|
425
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, first);
|
|
426
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, second);
|
|
427
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, first);
|
|
428
|
+
for (let index = 0; index < 5 && releases.size < 2; index++) await tick();
|
|
429
|
+
assert.equal(releases.size, 2);
|
|
430
|
+
assert.equal(executeCalls, 2);
|
|
431
|
+
releases.get("first")?.();
|
|
432
|
+
releases.get("second")?.();
|
|
433
|
+
while (responses.length < 2) await tick();
|
|
434
|
+
assert.deepEqual(responses.map(({ ownerRunId, nodeId, status }) => ({ ownerRunId, nodeId, status })).sort((a, b) => (a.ownerRunId ?? "").localeCompare(b.ownerRunId ?? "")), [
|
|
435
|
+
{ ownerRunId: "owner-1", nodeId: "node-1", status: "completed" },
|
|
436
|
+
{ ownerRunId: "owner-2", nodeId: "node-2", status: "completed" },
|
|
437
|
+
]);
|
|
438
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, first);
|
|
439
|
+
await tick();
|
|
440
|
+
assert.equal(executeCalls, 2);
|
|
441
|
+
assert.equal(responses.length, 2);
|
|
442
|
+
bridge.dispose();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("applies exact v2 pre-cancellation despite logical-node and bare-id conflicts", async () => {
|
|
446
|
+
const events = new FakeEvents();
|
|
447
|
+
const releases = new Map<string, () => void>();
|
|
448
|
+
const activeSignals = new Map<string, AbortSignal>();
|
|
449
|
+
const responses: SubagentDelegationV2Response[] = [];
|
|
450
|
+
events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => responses.push(payload as SubagentDelegationV2Response));
|
|
451
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
452
|
+
events,
|
|
453
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
454
|
+
execute: async () => { throw new Error("ordinary executor must remain separate"); },
|
|
455
|
+
executeVersioned: async (_id, params, signal) => {
|
|
456
|
+
if (typeof params.task !== "string") throw new Error("expected a single delegated task");
|
|
457
|
+
const task = params.task;
|
|
458
|
+
activeSignals.set(task, signal);
|
|
459
|
+
await new Promise<void>((resolve) => releases.set(task, resolve));
|
|
460
|
+
return { details: { mode: "single", results: [{ agent: params.agent, exitCode: 0, finalOutput: task }] } };
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const logicalOwner = { ...v2Request, requestId: "logical-owner", task: "logical-owner", result: { kind: "text" as const } };
|
|
465
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, logicalOwner);
|
|
466
|
+
while (!releases.has("logical-owner")) await tick();
|
|
467
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 2, requestId: "logical-pre", ownerRunId: "owner-1", nodeId: "node-1" });
|
|
468
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...logicalOwner, requestId: "logical-pre", task: "must-not-run" });
|
|
469
|
+
await tick();
|
|
470
|
+
assert.equal(responses.find((entry) => entry.requestId === "logical-pre")?.status, "cancelled");
|
|
471
|
+
assert.equal(activeSignals.get("logical-owner")?.aborted, false);
|
|
472
|
+
assert.equal(releases.has("must-not-run"), false);
|
|
473
|
+
|
|
474
|
+
const bareOwner = { ...v2Request, requestId: "shared-bare", ownerRunId: "owner-2", nodeId: "node-2", task: "bare-owner", result: { kind: "text" as const } };
|
|
475
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, bareOwner);
|
|
476
|
+
while (!releases.has("bare-owner")) await tick();
|
|
477
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 2, requestId: "shared-bare", ownerRunId: "owner-3", nodeId: "node-3" });
|
|
478
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...bareOwner, ownerRunId: "owner-3", nodeId: "node-3", task: "also-must-not-run" });
|
|
479
|
+
await tick();
|
|
480
|
+
assert.equal(responses.find((entry) => entry.ownerRunId === "owner-3")?.status, "cancelled");
|
|
481
|
+
assert.equal(activeSignals.get("bare-owner")?.aborted, false);
|
|
482
|
+
assert.equal(releases.has("also-must-not-run"), false);
|
|
483
|
+
|
|
484
|
+
releases.get("logical-owner")?.();
|
|
485
|
+
releases.get("bare-owner")?.();
|
|
486
|
+
bridge.dispose();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("suppresses v2 terminal events after disposal", async () => {
|
|
490
|
+
const events = new FakeEvents();
|
|
491
|
+
const responses: unknown[] = [];
|
|
492
|
+
events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => responses.push(payload));
|
|
493
|
+
let rejectExecution: ((error: Error) => void) | undefined;
|
|
494
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
495
|
+
events,
|
|
496
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
497
|
+
execute: async () => { throw new Error("ordinary executor must remain separate"); },
|
|
498
|
+
executeVersioned: async () => await new Promise((_resolve, reject) => { rejectExecution = reject; }),
|
|
499
|
+
});
|
|
500
|
+
const startedPromise = once(events, SUBAGENT_DELEGATION_STARTED_EVENT);
|
|
501
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...v2Request, requestId: "dispose-v2", result: { kind: "text" } });
|
|
502
|
+
await startedPromise;
|
|
503
|
+
bridge.dispose();
|
|
504
|
+
rejectExecution?.(new Error("aborted"));
|
|
505
|
+
await tick();
|
|
506
|
+
assert.deepEqual(responses, []);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("runs one v1 request through the existing executor and returns structured metadata", async () => {
|
|
510
|
+
const events = new FakeEvents();
|
|
511
|
+
let executeCalls = 0;
|
|
512
|
+
let observedRequest: unknown;
|
|
513
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
514
|
+
events,
|
|
515
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
516
|
+
execute: async (_requestId, delegatedRequest, _signal, _ctx, onUpdate) => {
|
|
517
|
+
executeCalls++;
|
|
518
|
+
observedRequest = delegatedRequest;
|
|
519
|
+
onUpdate({
|
|
520
|
+
details: {
|
|
521
|
+
mode: "single",
|
|
522
|
+
results: [{ agent: "reviewer", model: "openai/gpt-5" }],
|
|
523
|
+
progress: [{ index: 0, agent: "reviewer", currentTool: "read", recentOutput: ["line 1"], recentTools: [], toolCount: 1, tokens: 42, durationMs: 10 }],
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
details: {
|
|
528
|
+
mode: "single",
|
|
529
|
+
runId: "run-1",
|
|
530
|
+
results: [{
|
|
531
|
+
agent: "reviewer",
|
|
532
|
+
exitCode: 0,
|
|
533
|
+
model: "openai/gpt-5",
|
|
534
|
+
finalOutput: "done",
|
|
535
|
+
savedOutputPath: "/repo/result.md",
|
|
536
|
+
sessionFile: "/tmp/session.jsonl",
|
|
537
|
+
usage: { input: 2, output: 3, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 2 },
|
|
538
|
+
progressSummary: { toolCount: 4, tokens: 5, durationMs: 6 },
|
|
539
|
+
agentContract: { version: 1 },
|
|
540
|
+
execution: { status: "completed", success: true, exitCode: 0 },
|
|
541
|
+
acceptance: { status: "checked", evidenceStatus: "checked", explicit: true },
|
|
542
|
+
review: { status: "not-requested" },
|
|
543
|
+
effects: { fileMutation: { status: "missing", expected: true, attempted: false } },
|
|
544
|
+
skillsWarning: "Skills not found: review",
|
|
545
|
+
}],
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
const startedPromise = once(events, SUBAGENT_DELEGATION_STARTED_EVENT);
|
|
551
|
+
const updatePromise = once(events, SUBAGENT_DELEGATION_UPDATE_EVENT);
|
|
552
|
+
const responsePromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
553
|
+
|
|
554
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, request);
|
|
555
|
+
|
|
556
|
+
assert.deepEqual(await startedPromise, { version: 1, requestId: "delegation-1" });
|
|
557
|
+
assert.deepEqual(await updatePromise, {
|
|
558
|
+
version: 1,
|
|
559
|
+
requestId: "delegation-1",
|
|
560
|
+
currentTool: "read",
|
|
561
|
+
recentOutput: "line 1",
|
|
562
|
+
recentOutputLines: ["line 1"],
|
|
563
|
+
model: "openai/gpt-5",
|
|
564
|
+
toolCount: 1,
|
|
565
|
+
durationMs: 10,
|
|
566
|
+
tokens: 42,
|
|
567
|
+
});
|
|
568
|
+
const response = await responsePromise as SubagentDelegationResponse;
|
|
569
|
+
assert.equal(executeCalls, 1);
|
|
570
|
+
assert.deepEqual(observedRequest, {
|
|
571
|
+
agent: "reviewer",
|
|
572
|
+
task: "Review evidence",
|
|
573
|
+
context: "fresh",
|
|
574
|
+
cwd: "/repo",
|
|
575
|
+
model: "openai/gpt-5",
|
|
576
|
+
timeoutMs: 1_000,
|
|
577
|
+
turnBudget: { maxTurns: 4, graceTurns: 1 },
|
|
578
|
+
enforceHardTurnLimit: true,
|
|
579
|
+
toolBudget: { soft: 3, hard: 5, block: "*" },
|
|
580
|
+
skill: ["review"],
|
|
581
|
+
output: "result.md",
|
|
582
|
+
outputMode: "file-only",
|
|
583
|
+
outputSchema: { type: "object", properties: { ok: { type: "boolean" } } },
|
|
584
|
+
agentContract: { version: 1 },
|
|
585
|
+
acceptance: "checked",
|
|
586
|
+
artifacts: true,
|
|
587
|
+
async: false,
|
|
588
|
+
foregroundOnly: true,
|
|
589
|
+
clarify: false,
|
|
590
|
+
});
|
|
591
|
+
assert.equal(response.status, "completed");
|
|
592
|
+
assert.equal(response.runId, "run-1");
|
|
593
|
+
assert.equal(response.agent, "reviewer");
|
|
594
|
+
assert.equal(response.output, "done");
|
|
595
|
+
assert.equal(response.outputPath, "/repo/result.md");
|
|
596
|
+
assert.equal(response.sessionFile, "/tmp/session.jsonl");
|
|
597
|
+
assert.deepEqual(response.execution, { status: "completed", success: true, exitCode: 0 });
|
|
598
|
+
assert.deepEqual(response.review, { status: "not-requested" });
|
|
599
|
+
assert.deepEqual(response.effects, { fileMutation: { status: "missing", expected: true, attempted: false } });
|
|
600
|
+
assert.equal(response.turns, 2);
|
|
601
|
+
assert.equal(response.toolCount, 4);
|
|
602
|
+
assert.equal(response.tokens, 5);
|
|
603
|
+
assert.deepEqual(response.acceptance, { status: "checked", evidenceStatus: "checked", explicit: true });
|
|
604
|
+
assert.deepEqual(response.warnings, ["Skills not found: review"]);
|
|
605
|
+
bridge.dispose();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("routes independent v1 requests through the concurrent-safe delegated executor", async () => {
|
|
609
|
+
const events = new FakeEvents();
|
|
610
|
+
let ordinaryCalls = 0;
|
|
611
|
+
let active = 0;
|
|
612
|
+
let maxActive = 0;
|
|
613
|
+
let started = 0;
|
|
614
|
+
let release!: () => void;
|
|
615
|
+
const barrier = new Promise<void>((resolve) => { release = resolve; });
|
|
616
|
+
const responses: SubagentDelegationResponse[] = [];
|
|
617
|
+
events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (value) => responses.push(value as SubagentDelegationResponse));
|
|
618
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
619
|
+
events,
|
|
620
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
621
|
+
execute: async () => {
|
|
622
|
+
ordinaryCalls++;
|
|
623
|
+
return { details: { mode: "single", results: [] } };
|
|
624
|
+
},
|
|
625
|
+
executeVersioned: async (_id, params) => {
|
|
626
|
+
active++;
|
|
627
|
+
started++;
|
|
628
|
+
maxActive = Math.max(maxActive, active);
|
|
629
|
+
await barrier;
|
|
630
|
+
active--;
|
|
631
|
+
return {
|
|
632
|
+
details: {
|
|
633
|
+
mode: "single",
|
|
634
|
+
results: [{
|
|
635
|
+
agent: params.agent,
|
|
636
|
+
exitCode: 0,
|
|
637
|
+
finalOutput: params.task,
|
|
638
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 1 },
|
|
639
|
+
}],
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: "parallel-a", task: "A" });
|
|
645
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: "parallel-b", task: "B" });
|
|
646
|
+
while (started < 2) await tick();
|
|
647
|
+
assert.equal(maxActive, 2);
|
|
648
|
+
assert.equal(ordinaryCalls, 0);
|
|
649
|
+
release();
|
|
650
|
+
while (responses.length < 2) await tick();
|
|
651
|
+
assert.deepEqual(responses.map((response) => response.requestId).sort(), ["parallel-a", "parallel-b"]);
|
|
652
|
+
assert.ok(responses.every((response) => response.status === "completed"));
|
|
653
|
+
bridge.dispose();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("returns correlated invalid-request and unavailable-context statuses without executing", async () => {
|
|
657
|
+
const events = new FakeEvents();
|
|
658
|
+
let executeCalls = 0;
|
|
659
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
660
|
+
events,
|
|
661
|
+
getContext: () => null,
|
|
662
|
+
execute: async () => {
|
|
663
|
+
executeCalls++;
|
|
664
|
+
return { details: { mode: "single", results: [] } };
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
const invalidPromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
668
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, version: 2, requestId: "invalid-1" });
|
|
669
|
+
assert.deepEqual(await invalidPromise, {
|
|
670
|
+
version: 2,
|
|
671
|
+
requestId: "invalid-1",
|
|
672
|
+
status: "invalid_request",
|
|
673
|
+
error: "Unsupported delegation protocol version: 2.",
|
|
674
|
+
} satisfies SubagentDelegationV2InvalidResponse);
|
|
675
|
+
|
|
676
|
+
const unavailablePromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
677
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: "unavailable-1" });
|
|
678
|
+
const unavailable = await unavailablePromise as SubagentDelegationResponse;
|
|
679
|
+
assert.equal(unavailable.status, "unavailable_context");
|
|
680
|
+
assert.equal(executeCalls, 0);
|
|
681
|
+
bridge.dispose();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("maps terminal executor outcomes without failing inferred acceptance", async () => {
|
|
685
|
+
const cases = [
|
|
686
|
+
[{ timedOut: true }, "timed_out"],
|
|
687
|
+
[{ interrupted: true }, "interrupted"],
|
|
688
|
+
[{ stopped: true }, "interrupted"],
|
|
689
|
+
[{ turnBudgetExceeded: true }, "turn_budget_exhausted"],
|
|
690
|
+
[{ toolBudgetBlocked: true }, "tool_budget_exhausted"],
|
|
691
|
+
[{ turnBudgetExceeded: true, structuredOutputFailed: true }, "structured_output_failed"],
|
|
692
|
+
[{ acceptance: { status: "rejected", explicit: true } }, "acceptance_failed"],
|
|
693
|
+
[{ agentContract: { version: 1 }, acceptance: { status: "rejected", explicit: true } }, "completed"],
|
|
694
|
+
[{ acceptance: { status: "rejected", explicit: false } }, "completed"],
|
|
695
|
+
[{ exitCode: 1, error: "failed" }, "failed"],
|
|
696
|
+
] as const;
|
|
697
|
+
for (const [child, expectedStatus] of cases) {
|
|
698
|
+
const events = new FakeEvents();
|
|
699
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
700
|
+
events,
|
|
701
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
702
|
+
execute: async () => ({
|
|
703
|
+
isError: expectedStatus !== "completed",
|
|
704
|
+
details: {
|
|
705
|
+
mode: "single",
|
|
706
|
+
results: [{
|
|
707
|
+
agent: "reviewer",
|
|
708
|
+
exitCode: 0,
|
|
709
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
710
|
+
...child,
|
|
711
|
+
}],
|
|
712
|
+
},
|
|
713
|
+
}),
|
|
714
|
+
});
|
|
715
|
+
const responsePromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
716
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: `case-${expectedStatus}-${Math.random()}` });
|
|
717
|
+
assert.equal((await responsePromise as SubagentDelegationResponse).status, expectedStatus);
|
|
718
|
+
bridge.dispose();
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("ignores malformed versioned cancellation without aborting the owner", async () => {
|
|
723
|
+
const events = new FakeEvents();
|
|
724
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
725
|
+
events,
|
|
726
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
727
|
+
execute: async () => { throw new Error("ordinary executor must not own a strict v1 request"); },
|
|
728
|
+
executeVersioned: async (_id, _params, signal) => await new Promise((_resolve, reject) => {
|
|
729
|
+
signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
730
|
+
}),
|
|
731
|
+
});
|
|
732
|
+
const startedPromise = once(events, SUBAGENT_DELEGATION_STARTED_EVENT);
|
|
733
|
+
const responses: unknown[] = [];
|
|
734
|
+
events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => responses.push(payload));
|
|
735
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: "strict-cancel-1" });
|
|
736
|
+
await startedPromise;
|
|
737
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 2, requestId: "strict-cancel-1" });
|
|
738
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 1, requestId: "strict-cancel-1", reason: "extra" });
|
|
739
|
+
await tick();
|
|
740
|
+
assert.deepEqual(responses, []);
|
|
741
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 1, requestId: "strict-cancel-1" });
|
|
742
|
+
await tick();
|
|
743
|
+
assert.equal((responses[0] as SubagentDelegationResponse).status, "cancelled");
|
|
744
|
+
bridge.dispose();
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("preserves active request ownership when a duplicate requestId arrives", async () => {
|
|
748
|
+
const events = new FakeEvents();
|
|
749
|
+
let executeCalls = 0;
|
|
750
|
+
let finish: (() => void) | undefined;
|
|
751
|
+
const responses: unknown[] = [];
|
|
752
|
+
events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => responses.push(payload));
|
|
753
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
754
|
+
events,
|
|
755
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
756
|
+
execute: async () => {
|
|
757
|
+
executeCalls++;
|
|
758
|
+
await new Promise<void>((resolve) => { finish = resolve; });
|
|
759
|
+
return { details: { mode: "single", results: [{ agent: "reviewer", exitCode: 0, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 } }] } };
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
const startedPromise = once(events, SUBAGENT_DELEGATION_STARTED_EVENT);
|
|
763
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: "duplicate-1" });
|
|
764
|
+
await startedPromise;
|
|
765
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: "duplicate-1" });
|
|
766
|
+
await tick();
|
|
767
|
+
assert.equal(executeCalls, 1);
|
|
768
|
+
assert.equal(responses.length, 0);
|
|
769
|
+
finish?.();
|
|
770
|
+
await tick();
|
|
771
|
+
assert.equal(responses.length, 1);
|
|
772
|
+
assert.equal((responses[0] as SubagentDelegationResponse).status, "completed");
|
|
773
|
+
bridge.dispose();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it("bounds pending cancellation IDs and applies retained pre-cancellation once", async () => {
|
|
777
|
+
const events = new FakeEvents();
|
|
778
|
+
let executeCalls = 0;
|
|
779
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
780
|
+
events,
|
|
781
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
782
|
+
execute: async () => {
|
|
783
|
+
executeCalls++;
|
|
784
|
+
return { details: { mode: "single", results: [{ agent: "reviewer", exitCode: 0, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 } }] } };
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
for (let index = 0; index < 300; index++) {
|
|
788
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, { version: 1, requestId: `cancel-${index}` });
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const evictedPromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
792
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: "cancel-0" });
|
|
793
|
+
assert.equal((await evictedPromise as SubagentDelegationResponse).status, "completed");
|
|
794
|
+
|
|
795
|
+
const retainedPromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
796
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: "cancel-299" });
|
|
797
|
+
assert.equal((await retainedPromise as SubagentDelegationResponse).status, "cancelled");
|
|
798
|
+
assert.equal(executeCalls, 1);
|
|
799
|
+
bridge.dispose();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it("fails closed instead of evicting exact v2 cancellation identity", async () => {
|
|
803
|
+
const events = new FakeEvents();
|
|
804
|
+
let executeCalls = 0;
|
|
805
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
806
|
+
events,
|
|
807
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
808
|
+
execute: async () => {
|
|
809
|
+
executeCalls++;
|
|
810
|
+
return { details: { mode: "single", results: [{ agent: "reviewer", exitCode: 0, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 } }] } };
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
for (let index = 0; index < 8_192; index++) {
|
|
814
|
+
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, {
|
|
815
|
+
version: 2,
|
|
816
|
+
requestId: `attempt-${index}`,
|
|
817
|
+
ownerRunId: "saturated-owner",
|
|
818
|
+
nodeId: `node-${index}`,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const retainedPromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
823
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, {
|
|
824
|
+
...v2Request,
|
|
825
|
+
requestId: "attempt-0",
|
|
826
|
+
ownerRunId: "saturated-owner",
|
|
827
|
+
nodeId: "node-0",
|
|
828
|
+
});
|
|
829
|
+
assert.equal((await retainedPromise as SubagentDelegationV2Response).status, "cancelled");
|
|
830
|
+
|
|
831
|
+
const overflowPromise = once(events, SUBAGENT_DELEGATION_RESPONSE_EVENT);
|
|
832
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, {
|
|
833
|
+
...v2Request,
|
|
834
|
+
requestId: "overflow-attempt",
|
|
835
|
+
ownerRunId: "saturated-owner",
|
|
836
|
+
nodeId: "overflow-node",
|
|
837
|
+
});
|
|
838
|
+
const overflow = await overflowPromise as SubagentDelegationV2Response;
|
|
839
|
+
assert.equal(overflow.status, "unavailable_context");
|
|
840
|
+
assert.match(overflow.error ?? "", /identity capacity/i);
|
|
841
|
+
assert.equal(executeCalls, 0);
|
|
842
|
+
bridge.dispose();
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it("fails closed when settled v2 identity history reaches capacity", async () => {
|
|
846
|
+
const events = new FakeEvents();
|
|
847
|
+
const responses: SubagentDelegationV2Response[] = [];
|
|
848
|
+
let executeCalls = 0;
|
|
849
|
+
events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => responses.push(payload as SubagentDelegationV2Response));
|
|
850
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
851
|
+
events,
|
|
852
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
853
|
+
execute: async (requestId) => {
|
|
854
|
+
executeCalls++;
|
|
855
|
+
return { details: { mode: "single", results: [{ agent: "reviewer", exitCode: 0, finalOutput: requestId }] } };
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
for (let index = 0; index < 8_192; index++) {
|
|
859
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, {
|
|
860
|
+
...v2Request,
|
|
861
|
+
requestId: `settled-${index}`,
|
|
862
|
+
ownerRunId: "settled-owner",
|
|
863
|
+
nodeId: `settled-node-${index}`,
|
|
864
|
+
result: { kind: "text" },
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
for (let attempt = 0; responses.length < 8_192 && attempt < 100; attempt++) await tick();
|
|
868
|
+
assert.equal(responses.length, 8_192);
|
|
869
|
+
assert.equal(executeCalls, 8_192);
|
|
870
|
+
|
|
871
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, {
|
|
872
|
+
...v2Request,
|
|
873
|
+
requestId: "settled-overflow",
|
|
874
|
+
ownerRunId: "settled-owner",
|
|
875
|
+
nodeId: "settled-overflow-node",
|
|
876
|
+
result: { kind: "text" },
|
|
877
|
+
});
|
|
878
|
+
await tick();
|
|
879
|
+
assert.equal(responses.at(-1)?.status, "unavailable_context");
|
|
880
|
+
assert.equal(executeCalls, 8_192);
|
|
881
|
+
|
|
882
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, {
|
|
883
|
+
...v2Request,
|
|
884
|
+
requestId: "settled-0",
|
|
885
|
+
ownerRunId: "settled-owner",
|
|
886
|
+
nodeId: "settled-node-0",
|
|
887
|
+
result: { kind: "text" },
|
|
888
|
+
});
|
|
889
|
+
await tick();
|
|
890
|
+
assert.equal(responses.length, 8_193);
|
|
891
|
+
assert.equal(executeCalls, 8_192);
|
|
892
|
+
bridge.dispose();
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it("suppresses stale terminal events after disposal", async () => {
|
|
896
|
+
const events = new FakeEvents();
|
|
897
|
+
const responses: unknown[] = [];
|
|
898
|
+
events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => responses.push(payload));
|
|
899
|
+
let rejectExecution: ((error: Error) => void) | undefined;
|
|
900
|
+
const bridge = registerPromptTemplateDelegationBridge({
|
|
901
|
+
events,
|
|
902
|
+
getContext: () => ({ cwd: "/repo" }),
|
|
903
|
+
execute: async () => await new Promise((_resolve, reject) => { rejectExecution = reject; }),
|
|
904
|
+
});
|
|
905
|
+
const startedPromise = once(events, SUBAGENT_DELEGATION_STARTED_EVENT);
|
|
906
|
+
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, { ...request, requestId: "dispose-1" });
|
|
907
|
+
await startedPromise;
|
|
908
|
+
bridge.dispose();
|
|
909
|
+
rejectExecution?.(new Error("aborted"));
|
|
910
|
+
await tick();
|
|
911
|
+
assert.deepEqual(responses, []);
|
|
912
|
+
});
|
|
913
|
+
});
|