macro-agent 0.0.17 → 0.1.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/.claude/settings.local.json +3 -1
- package/.sudocode/specs.jsonl +4 -0
- package/CLAUDE.md +16 -14
- package/README.md +11 -29
- package/dist/acp/macro-agent.d.ts +17 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +183 -55
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/types.d.ts +32 -1
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +65 -1
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js +464 -183
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +37 -6
- package/dist/api/server.js.map +1 -1
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/token.d.ts +41 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/auth/token.js +73 -0
- package/dist/auth/token.js.map +1 -0
- package/dist/cli/acp.d.ts +2 -23
- package/dist/cli/acp.d.ts.map +1 -1
- package/dist/cli/acp.js +127 -61
- package/dist/cli/acp.js.map +1 -1
- package/dist/cli/index.js +147 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts +6 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +268 -181
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/parse-args.d.ts +20 -0
- package/dist/cli/parse-args.d.ts.map +1 -0
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli/parse-args.js.map +1 -0
- package/dist/cli/stable-instance-id.d.ts +8 -0
- package/dist/cli/stable-instance-id.d.ts.map +1 -0
- package/dist/cli/stable-instance-id.js +14 -0
- package/dist/cli/stable-instance-id.js.map +1 -0
- package/dist/config/project-config.d.ts +74 -7
- package/dist/config/project-config.d.ts.map +1 -1
- package/dist/config/project-config.js +123 -20
- package/dist/config/project-config.js.map +1 -1
- package/dist/map/adapter/acp-over-map.d.ts +23 -0
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
- package/dist/map/adapter/acp-over-map.js +482 -55
- package/dist/map/adapter/acp-over-map.js.map +1 -1
- package/dist/map/adapter/connection-manager.d.ts.map +1 -1
- package/dist/map/adapter/connection-manager.js +3 -0
- package/dist/map/adapter/connection-manager.js.map +1 -1
- package/dist/map/adapter/event-log.d.ts +87 -0
- package/dist/map/adapter/event-log.d.ts.map +1 -0
- package/dist/map/adapter/event-log.js +122 -0
- package/dist/map/adapter/event-log.js.map +1 -0
- package/dist/map/adapter/event-translator.js +6 -6
- package/dist/map/adapter/event-translator.js.map +1 -1
- package/dist/map/adapter/extensions/agent-lifecycle.d.ts +82 -0
- package/dist/map/adapter/extensions/agent-lifecycle.d.ts.map +1 -0
- package/dist/map/adapter/extensions/agent-lifecycle.js +164 -0
- package/dist/map/adapter/extensions/agent-lifecycle.js.map +1 -0
- package/dist/map/adapter/extensions/index.d.ts +10 -1
- package/dist/map/adapter/extensions/index.d.ts.map +1 -1
- package/dist/map/adapter/extensions/index.js +34 -0
- package/dist/map/adapter/extensions/index.js.map +1 -1
- package/dist/map/adapter/extensions/mcp-bridge.d.ts +57 -0
- package/dist/map/adapter/extensions/mcp-bridge.d.ts.map +1 -0
- package/dist/map/adapter/extensions/mcp-bridge.js +745 -0
- package/dist/map/adapter/extensions/mcp-bridge.js.map +1 -0
- package/dist/map/adapter/extensions/rename.d.ts +29 -0
- package/dist/map/adapter/extensions/rename.d.ts.map +1 -0
- package/dist/map/adapter/extensions/rename.js +49 -0
- package/dist/map/adapter/extensions/rename.js.map +1 -0
- package/dist/map/adapter/extensions/task.d.ts.map +1 -1
- package/dist/map/adapter/extensions/task.js +10 -0
- package/dist/map/adapter/extensions/task.js.map +1 -1
- package/dist/map/adapter/extensions/update-metadata.d.ts +29 -0
- package/dist/map/adapter/extensions/update-metadata.d.ts.map +1 -0
- package/dist/map/adapter/extensions/update-metadata.js +67 -0
- package/dist/map/adapter/extensions/update-metadata.js.map +1 -0
- package/dist/map/adapter/index.d.ts +2 -1
- package/dist/map/adapter/index.d.ts.map +1 -1
- package/dist/map/adapter/index.js +8 -2
- package/dist/map/adapter/index.js.map +1 -1
- package/dist/map/adapter/interface.d.ts +2 -0
- package/dist/map/adapter/interface.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.d.ts +4 -0
- package/dist/map/adapter/map-adapter.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.js +302 -30
- package/dist/map/adapter/map-adapter.js.map +1 -1
- package/dist/map/adapter/subscription-manager.d.ts.map +1 -1
- package/dist/map/adapter/subscription-manager.js +5 -1
- package/dist/map/adapter/subscription-manager.js.map +1 -1
- package/dist/map/adapter/types.d.ts +2 -0
- package/dist/map/adapter/types.d.ts.map +1 -1
- package/dist/mcp/map-client.d.ts +39 -0
- package/dist/mcp/map-client.d.ts.map +1 -0
- package/dist/mcp/map-client.js +129 -0
- package/dist/mcp/map-client.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +14 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +113 -85
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/mcp/types.d.ts +9 -1
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/mcp/types.js.map +1 -1
- package/dist/metrics/metrics.js +1 -1
- package/dist/metrics/metrics.js.map +1 -1
- package/dist/roles/capabilities.d.ts +3 -1
- package/dist/roles/capabilities.d.ts.map +1 -1
- package/dist/roles/capabilities.js +17 -7
- package/dist/roles/capabilities.js.map +1 -1
- package/dist/roles/config-loader.d.ts +6 -6
- package/dist/roles/config-loader.d.ts.map +1 -1
- package/dist/roles/config-loader.js +6 -6
- package/dist/roles/config-loader.js.map +1 -1
- package/dist/roles/registry.d.ts +2 -2
- package/dist/roles/registry.js +2 -2
- package/dist/server/combined-server.d.ts +20 -0
- package/dist/server/combined-server.d.ts.map +1 -1
- package/dist/server/combined-server.js +107 -8
- package/dist/server/combined-server.js.map +1 -1
- package/dist/store/event-store.d.ts +7 -1
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +91 -8
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/types/agents.d.ts +23 -0
- package/dist/store/types/agents.d.ts.map +1 -1
- package/dist/store/types/events.d.ts +1 -1
- package/dist/store/types/events.d.ts.map +1 -1
- package/dist/task/backend/index.d.ts +47 -29
- package/dist/task/backend/index.d.ts.map +1 -1
- package/dist/task/backend/index.js +109 -71
- package/dist/task/backend/index.js.map +1 -1
- package/dist/task/backend/memory.d.ts +1 -0
- package/dist/task/backend/memory.d.ts.map +1 -1
- package/dist/task/backend/memory.js +3 -0
- package/dist/task/backend/memory.js.map +1 -1
- package/dist/task/backend/opentasks/backend.d.ts +140 -0
- package/dist/task/backend/opentasks/backend.d.ts.map +1 -0
- package/dist/task/backend/opentasks/backend.js +1023 -0
- package/dist/task/backend/opentasks/backend.js.map +1 -0
- package/dist/task/backend/opentasks/client.d.ts +337 -0
- package/dist/task/backend/opentasks/client.d.ts.map +1 -0
- package/dist/task/backend/opentasks/client.js +225 -0
- package/dist/task/backend/opentasks/client.js.map +1 -0
- package/dist/task/backend/opentasks/daemon-manager.d.ts +89 -0
- package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -0
- package/dist/task/backend/opentasks/daemon-manager.js +195 -0
- package/dist/task/backend/opentasks/daemon-manager.js.map +1 -0
- package/dist/task/backend/opentasks/index.d.ts +21 -0
- package/dist/task/backend/opentasks/index.d.ts.map +1 -0
- package/dist/task/backend/opentasks/index.js +21 -0
- package/dist/task/backend/opentasks/index.js.map +1 -0
- package/dist/task/backend/opentasks/mapping.d.ts +48 -0
- package/dist/task/backend/opentasks/mapping.d.ts.map +1 -0
- package/dist/task/backend/opentasks/mapping.js +77 -0
- package/dist/task/backend/opentasks/mapping.js.map +1 -0
- package/dist/task/backend/types.d.ts +33 -53
- package/dist/task/backend/types.d.ts.map +1 -1
- package/dist/task/backend/types.js +7 -11
- package/dist/task/backend/types.js.map +1 -1
- package/dist/task/backend/unified-tool-provider.d.ts +57 -0
- package/dist/task/backend/unified-tool-provider.d.ts.map +1 -0
- package/dist/task/backend/unified-tool-provider.js +623 -0
- package/dist/task/backend/unified-tool-provider.js.map +1 -0
- package/dist/teams/team-loader.d.ts +2 -2
- package/dist/teams/team-loader.js +3 -3
- package/dist/teams/team-loader.js.map +1 -1
- package/dist/teams/team-runtime.d.ts.map +1 -1
- package/dist/teams/team-runtime.js +2 -0
- package/dist/teams/team-runtime.js.map +1 -1
- package/docs/architecture.md +7 -6
- package/docs/configuration.md +26 -62
- package/docs/implementation-details.md +5 -5
- package/docs/implementation-summary.md +17 -17
- package/docs/plan-self-driving-support.md +4 -4
- package/docs/spec-self-driving-support.md +10 -10
- package/docs/team-templates.md +2 -2
- package/docs/teams.md +3 -3
- package/docs/troubleshooting.md +10 -11
- package/package.json +6 -4
- package/src/__tests__/e2e/agent-spawn-visibility.e2e.test.ts +761 -0
- package/src/__tests__/e2e/full-agent-conflict-resolution.e2e.test.ts +2 -2
- package/src/__tests__/e2e/mcp-thin-client-bridge.e2e.test.ts +304 -0
- package/src/__tests__/e2e/mcp-tools-available.e2e.test.ts +324 -0
- package/src/__tests__/e2e/multi-agent.e2e.test.ts +5 -5
- package/src/__tests__/e2e/spawn-session-streaming.e2e.test.ts +563 -0
- package/src/acp/__tests__/history.test.ts +8 -4
- package/src/acp/__tests__/integration.test.ts +56 -31
- package/src/acp/__tests__/macro-agent.test.ts +16 -7
- package/src/acp/macro-agent.ts +230 -62
- package/src/acp/types.ts +46 -1
- package/src/agent/__tests__/agent-manager.test.ts +228 -2
- package/src/agent/agent-manager.ts +714 -261
- package/src/agent/types.ts +3 -1
- package/src/api/server.ts +41 -7
- package/src/auth/__tests__/token.test.ts +100 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/token.ts +82 -0
- package/src/cli/__tests__/acp.test.ts +1 -1
- package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
- package/src/cli/acp.ts +130 -72
- package/src/cli/index.ts +120 -14
- package/src/cli/mcp.ts +311 -207
- package/src/cli/parse-args.ts +54 -0
- package/src/cli/stable-instance-id.ts +14 -0
- package/src/config/project-config.ts +190 -27
- package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
- package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +820 -0
- package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
- package/src/map/adapter/__tests__/acp-over-map-history.test.ts +724 -2
- package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
- package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
- package/src/map/adapter/__tests__/event-log.test.ts +527 -0
- package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
- package/src/map/adapter/__tests__/extensions.test.ts +408 -0
- package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
- package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
- package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
- package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
- package/src/map/adapter/acp-over-map.ts +777 -92
- package/src/map/adapter/connection-manager.ts +3 -0
- package/src/map/adapter/event-log.ts +208 -0
- package/src/map/adapter/event-translator.ts +6 -6
- package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
- package/src/map/adapter/extensions/index.ts +60 -0
- package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
- package/src/map/adapter/extensions/task.ts +11 -0
- package/src/map/adapter/extensions/update-metadata.ts +126 -0
- package/src/map/adapter/index.ts +28 -0
- package/src/map/adapter/interface.ts +2 -0
- package/src/map/adapter/map-adapter.ts +373 -38
- package/src/map/adapter/subscription-manager.ts +5 -1
- package/src/map/adapter/types.ts +2 -0
- package/src/mcp/__tests__/map-client.test.ts +386 -0
- package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
- package/src/mcp/__tests__/mcp-server.test.ts +100 -1
- package/src/mcp/map-client.ts +177 -0
- package/src/mcp/mcp-server.ts +191 -100
- package/src/mcp/types.ts +6 -1
- package/src/metrics/metrics.ts +1 -1
- package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
- package/src/roles/__tests__/config-loader.test.ts +7 -7
- package/src/roles/capabilities.ts +17 -7
- package/src/roles/config-loader.ts +6 -6
- package/src/roles/registry.ts +2 -2
- package/src/server/__tests__/combined-server.test.ts +94 -21
- package/src/server/combined-server.ts +189 -33
- package/src/steering/__tests__/steering-integration.test.ts +1 -1
- package/src/store/__tests__/event-store.test.ts +236 -1
- package/src/store/__tests__/instance.test.ts +3 -3
- package/src/store/event-store.ts +109 -8
- package/src/store/types/agents.ts +16 -0
- package/src/store/types/events.ts +1 -1
- package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
- package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
- package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
- package/src/task/backend/index.ts +156 -106
- package/src/task/backend/memory.ts +4 -0
- package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
- package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
- package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
- package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
- package/src/task/backend/opentasks/backend.ts +1323 -0
- package/src/task/backend/opentasks/client.ts +652 -0
- package/src/task/backend/opentasks/daemon-manager.ts +253 -0
- package/src/task/backend/opentasks/index.ts +69 -0
- package/src/task/backend/opentasks/mapping.ts +94 -0
- package/src/task/backend/types.ts +42 -66
- package/src/task/backend/unified-tool-provider.ts +779 -0
- package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
- package/src/teams/team-loader.ts +3 -3
- package/src/teams/team-runtime.ts +2 -0
- package/test_fixtures/README.md +2 -3
- package/test_fixtures/fixtures/index.ts +0 -3
- package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
- package/test_fixtures/fixtures/repos/index.ts +1 -3
- package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
- package/test_fixtures/fixtures/repos/types.ts +0 -11
- package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
- package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
- package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
- package/vitest.config.ts +1 -1
- package/vitest.e2e.config.ts +1 -1
- package/vitest.setup.ts +1 -30
- package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
- package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
- package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
- package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
- package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
- package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
- package/.macro-agent/teams/self-driving/team.yaml +0 -103
- package/.macro-agent/teams/structured/prompts/developer.md +0 -26
- package/.macro-agent/teams/structured/prompts/lead.md +0 -25
- package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
- package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
- package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
- package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
- package/.macro-agent/teams/structured/team.yaml +0 -89
- package/docs/sudocode-integration.md +0 -383
- package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
- package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
- package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
- package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
- package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
- package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
- package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
- package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
- package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
- package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
- package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
- package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
- package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
- package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
- package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
- package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
- package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
- package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
- package/src/task/backend/sudocode/backend.ts +0 -1237
- package/src/task/backend/sudocode/client.ts +0 -515
- package/src/task/backend/sudocode/index.ts +0 -120
- package/src/task/backend/sudocode/mapping.ts +0 -93
- package/src/task/backend/sudocode/server-client.ts +0 -522
- package/src/task/backend/sudocode/standalone-client.ts +0 -623
- package/src/task/backend/sudocode/sync-policy.ts +0 -387
- package/src/task/backend/sudocode/tools.ts +0 -896
- package/src/task/backend/tool-provider.ts +0 -506
- package/test_fixtures/fixtures/sudocode/index.ts +0 -29
- package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
- package/test_fixtures/fixtures/sudocode/specs.ts +0 -159
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP-over-MAP Cancel & Stop Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests two distinct cancellation mechanisms:
|
|
5
|
+
* 1. ACP cancel (session/cancel) - soft cancel: aborts the streaming loop and
|
|
6
|
+
* calls session.cancel() on the subprocess, but keeps the agent alive.
|
|
7
|
+
* 2. MAP stop (map/agents/stop via handleStopAgent) - hard kill: terminates
|
|
8
|
+
* the agent subprocess, deallocates resources, cascade-terminates children.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, afterEach, vi, beforeEach } from "vitest";
|
|
12
|
+
import { ACPOverMAPHandler } from "../acp-over-map.js";
|
|
13
|
+
import type { ACPEnvelope } from "../acp-over-map.js";
|
|
14
|
+
import {
|
|
15
|
+
createMAPAdapter,
|
|
16
|
+
MAPAdapterImpl,
|
|
17
|
+
type MAPAdapterServices,
|
|
18
|
+
} from "../map-adapter.js";
|
|
19
|
+
import type { MAPAdapter } from "../interface.js";
|
|
20
|
+
import type { ParticipantId, EventNotification } from "../types.js";
|
|
21
|
+
import { createEventStore, type EventStore } from "../../../store/event-store.js";
|
|
22
|
+
import type { AgentManager } from "../../../agent/agent-manager.js";
|
|
23
|
+
import type { TaskManager } from "../../../task/task-manager.js";
|
|
24
|
+
import type { Agent, Task } from "../../../store/types/index.js";
|
|
25
|
+
import type { AgentId } from "../../../store/types/index.js";
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────
|
|
28
|
+
// Helpers
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function createMockAgent(overrides: Partial<Agent> = {}): Agent {
|
|
32
|
+
return {
|
|
33
|
+
id: "agent-1" as AgentId,
|
|
34
|
+
session_id: "session-1",
|
|
35
|
+
state: "running",
|
|
36
|
+
task: "Test task",
|
|
37
|
+
task_id: "task-1",
|
|
38
|
+
parent: null,
|
|
39
|
+
lineage: [],
|
|
40
|
+
config: {},
|
|
41
|
+
cwd: "/test/cwd",
|
|
42
|
+
created_at: Date.now(),
|
|
43
|
+
started_at: Date.now(),
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createMockTask(overrides: Partial<Task> = {}): Task {
|
|
49
|
+
return {
|
|
50
|
+
id: "task-1",
|
|
51
|
+
description: "Test task",
|
|
52
|
+
status: "in_progress",
|
|
53
|
+
created_by: "agent-1",
|
|
54
|
+
created_at: Date.now(),
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a mock AgentManager that yields the given updates from prompt().
|
|
61
|
+
* The mock session object has a cancel() method for testing ACP cancel.
|
|
62
|
+
*/
|
|
63
|
+
function createMockAgentManager(
|
|
64
|
+
promptUpdates: unknown[] = [],
|
|
65
|
+
options?: { slowYield?: number },
|
|
66
|
+
): { agentManager: AgentManager; mockSession: { cancel: ReturnType<typeof vi.fn> } } {
|
|
67
|
+
const mockAgent = createMockAgent();
|
|
68
|
+
const mockSession = {
|
|
69
|
+
cancel: vi.fn().mockResolvedValue(undefined),
|
|
70
|
+
prompt: vi.fn(),
|
|
71
|
+
id: "session-1",
|
|
72
|
+
cwd: "/test/cwd",
|
|
73
|
+
modes: [],
|
|
74
|
+
models: [],
|
|
75
|
+
isProcessing: false,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const agentManager = {
|
|
79
|
+
spawn: vi.fn().mockResolvedValue({
|
|
80
|
+
id: "agent-new",
|
|
81
|
+
session_id: "session-new",
|
|
82
|
+
agent: createMockAgent({ id: "agent-new" as AgentId, session_id: "session-new" }),
|
|
83
|
+
session: mockSession,
|
|
84
|
+
}),
|
|
85
|
+
get: vi.fn().mockReturnValue(mockAgent),
|
|
86
|
+
list: vi.fn().mockReturnValue([mockAgent]),
|
|
87
|
+
listHeadManagers: vi.fn().mockReturnValue([mockAgent]),
|
|
88
|
+
getChildren: vi.fn().mockReturnValue([]),
|
|
89
|
+
getHierarchy: vi.fn().mockReturnValue({
|
|
90
|
+
root: { agent: mockAgent, children: [] },
|
|
91
|
+
depth: 1,
|
|
92
|
+
totalAgents: 1,
|
|
93
|
+
}),
|
|
94
|
+
getOrCreateHeadManager: vi.fn().mockResolvedValue({
|
|
95
|
+
id: "agent-1",
|
|
96
|
+
session_id: "session-1",
|
|
97
|
+
agent: mockAgent,
|
|
98
|
+
session: mockSession,
|
|
99
|
+
}),
|
|
100
|
+
hasActiveSession: vi.fn().mockReturnValue(true),
|
|
101
|
+
resume: vi.fn().mockResolvedValue({
|
|
102
|
+
id: "agent-1",
|
|
103
|
+
session_id: "session-1",
|
|
104
|
+
agent: mockAgent,
|
|
105
|
+
session: mockSession,
|
|
106
|
+
}),
|
|
107
|
+
terminate: vi.fn().mockResolvedValue(undefined),
|
|
108
|
+
prompt: vi.fn().mockReturnValue({
|
|
109
|
+
[Symbol.asyncIterator]: async function* () {
|
|
110
|
+
for (const update of promptUpdates) {
|
|
111
|
+
if (options?.slowYield) {
|
|
112
|
+
await new Promise((r) => setTimeout(r, options.slowYield));
|
|
113
|
+
}
|
|
114
|
+
yield update;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
getSession: vi.fn().mockReturnValue(mockSession),
|
|
119
|
+
onLifecycleEvent: vi.fn().mockReturnValue(() => {}),
|
|
120
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
121
|
+
respondToPermission: vi.fn().mockReturnValue(true),
|
|
122
|
+
cancelPermission: vi.fn().mockReturnValue(true),
|
|
123
|
+
} as unknown as AgentManager;
|
|
124
|
+
|
|
125
|
+
return { agentManager, mockSession };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createMockTaskManager(): TaskManager {
|
|
129
|
+
return {
|
|
130
|
+
get: vi.fn().mockReturnValue(createMockTask()),
|
|
131
|
+
list: vi.fn().mockReturnValue([createMockTask()]),
|
|
132
|
+
create: vi.fn().mockReturnValue(createMockTask()),
|
|
133
|
+
} as unknown as TaskManager;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Build an ACP envelope for processRequest */
|
|
137
|
+
function envelope(
|
|
138
|
+
streamId: string,
|
|
139
|
+
method: string,
|
|
140
|
+
params?: unknown,
|
|
141
|
+
sessionId?: string,
|
|
142
|
+
): ACPEnvelope {
|
|
143
|
+
return {
|
|
144
|
+
acp: {
|
|
145
|
+
jsonrpc: "2.0",
|
|
146
|
+
id: `${streamId}-${method}-${Date.now()}`,
|
|
147
|
+
method,
|
|
148
|
+
params,
|
|
149
|
+
},
|
|
150
|
+
acpContext: {
|
|
151
|
+
streamId,
|
|
152
|
+
sessionId,
|
|
153
|
+
direction: "client-to-agent",
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────
|
|
159
|
+
// Tests
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe("ACP-over-MAP cancel and stop", () => {
|
|
163
|
+
let eventStore: EventStore;
|
|
164
|
+
let handler: ACPOverMAPHandler;
|
|
165
|
+
|
|
166
|
+
afterEach(async () => {
|
|
167
|
+
if (eventStore) {
|
|
168
|
+
await eventStore.close();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
async function setup(
|
|
173
|
+
promptUpdates: unknown[] = [],
|
|
174
|
+
options?: { slowYield?: number },
|
|
175
|
+
) {
|
|
176
|
+
eventStore = await createEventStore({ inMemory: true });
|
|
177
|
+
const { agentManager, mockSession } = createMockAgentManager(promptUpdates, options);
|
|
178
|
+
const taskManager = createMockTaskManager();
|
|
179
|
+
|
|
180
|
+
handler = new ACPOverMAPHandler({
|
|
181
|
+
agentManager,
|
|
182
|
+
eventStore,
|
|
183
|
+
taskManager,
|
|
184
|
+
defaultCwd: "/test/cwd",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return { agentManager, taskManager, mockSession };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Register an agent in the EventStore so loadSession can resolve it */
|
|
191
|
+
function registerAgent(agentId: string, sessionId: string): void {
|
|
192
|
+
eventStore.emit({
|
|
193
|
+
type: "spawn",
|
|
194
|
+
source: { agent_id: agentId },
|
|
195
|
+
payload: {
|
|
196
|
+
agent_id: agentId,
|
|
197
|
+
session_id: sessionId,
|
|
198
|
+
task: "Test task",
|
|
199
|
+
task_id: "task-1",
|
|
200
|
+
cwd: "/test/cwd",
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
eventStore.emit({
|
|
204
|
+
type: "lifecycle",
|
|
205
|
+
source: { agent_id: agentId },
|
|
206
|
+
payload: {
|
|
207
|
+
agent_id: agentId,
|
|
208
|
+
action: "started",
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Initialize a stream and create a session, returning the sessionId */
|
|
214
|
+
async function initAndCreateSession(
|
|
215
|
+
streamId: string,
|
|
216
|
+
targetAgentId: AgentId = "agent-1" as AgentId,
|
|
217
|
+
): Promise<string> {
|
|
218
|
+
await handler.processRequest(
|
|
219
|
+
targetAgentId,
|
|
220
|
+
envelope(streamId, "initialize", {
|
|
221
|
+
protocolVersion: 1,
|
|
222
|
+
capabilities: {},
|
|
223
|
+
clientInfo: { name: "test", version: "1.0" },
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const sessionResult = await handler.processRequest(
|
|
228
|
+
targetAgentId,
|
|
229
|
+
envelope(streamId, "session/new", {
|
|
230
|
+
cwd: "/test",
|
|
231
|
+
mcpServers: [],
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const sessionId = (sessionResult.acp.result as { sessionId?: string })?.sessionId;
|
|
236
|
+
if (!sessionId) throw new Error("session/new did not return sessionId");
|
|
237
|
+
|
|
238
|
+
registerAgent(targetAgentId, sessionId);
|
|
239
|
+
|
|
240
|
+
return sessionId;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────
|
|
244
|
+
// ACP Cancel (session/cancel) — soft cancel
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
describe("ACP cancel (session/cancel)", () => {
|
|
248
|
+
it("should return cancelled:true", async () => {
|
|
249
|
+
await setup();
|
|
250
|
+
|
|
251
|
+
const streamId = "cancel-test-1";
|
|
252
|
+
const agentId = "agent-1" as AgentId;
|
|
253
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
254
|
+
|
|
255
|
+
const result = await handler.processRequest(
|
|
256
|
+
agentId,
|
|
257
|
+
envelope(streamId, "session/cancel", { sessionId }, sessionId),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(result.acp.result).toEqual({ cancelled: true });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should call session.cancel() on the agent's active session", async () => {
|
|
264
|
+
const { mockSession } = await setup();
|
|
265
|
+
|
|
266
|
+
const streamId = "cancel-test-2";
|
|
267
|
+
const agentId = "agent-1" as AgentId;
|
|
268
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
269
|
+
|
|
270
|
+
await handler.processRequest(
|
|
271
|
+
agentId,
|
|
272
|
+
envelope(streamId, "session/cancel", { sessionId }, sessionId),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
expect(mockSession.cancel).toHaveBeenCalledTimes(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should NOT call agentManager.terminate()", async () => {
|
|
279
|
+
const { agentManager } = await setup();
|
|
280
|
+
|
|
281
|
+
const streamId = "cancel-test-3";
|
|
282
|
+
const agentId = "agent-1" as AgentId;
|
|
283
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
284
|
+
|
|
285
|
+
await handler.processRequest(
|
|
286
|
+
agentId,
|
|
287
|
+
envelope(streamId, "session/cancel", { sessionId }, sessionId),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(agentManager.terminate).not.toHaveBeenCalled();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should abort the stream's abort controller", async () => {
|
|
294
|
+
await setup();
|
|
295
|
+
|
|
296
|
+
const streamId = "cancel-test-4";
|
|
297
|
+
const agentId = "agent-1" as AgentId;
|
|
298
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
299
|
+
|
|
300
|
+
await handler.processRequest(
|
|
301
|
+
agentId,
|
|
302
|
+
envelope(streamId, "session/cancel", { sessionId }, sessionId),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Sending a prompt after cancel should reset the abort controller and work
|
|
306
|
+
// (the handler resets it on new prompt if aborted)
|
|
307
|
+
const promptResult = await handler.processRequest(
|
|
308
|
+
agentId,
|
|
309
|
+
envelope(streamId, "session/prompt", {
|
|
310
|
+
prompt: [{ type: "text", text: "Hello after cancel" }],
|
|
311
|
+
}, sessionId),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(promptResult.acp.result).toBeDefined();
|
|
315
|
+
expect(promptResult.acp.error).toBeUndefined();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should handle cancel gracefully when no session exists", async () => {
|
|
319
|
+
const { agentManager } = await setup();
|
|
320
|
+
// Mock getSession returning null for this case
|
|
321
|
+
(agentManager.getSession as ReturnType<typeof vi.fn>).mockReturnValue(null);
|
|
322
|
+
|
|
323
|
+
const streamId = "cancel-test-5";
|
|
324
|
+
const agentId = "agent-1" as AgentId;
|
|
325
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
326
|
+
|
|
327
|
+
const result = await handler.processRequest(
|
|
328
|
+
agentId,
|
|
329
|
+
envelope(streamId, "session/cancel", { sessionId }, sessionId),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Should still succeed even without an active session
|
|
333
|
+
expect(result.acp.result).toEqual({ cancelled: true });
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should handle session.cancel() failure gracefully", async () => {
|
|
337
|
+
const { mockSession } = await setup();
|
|
338
|
+
mockSession.cancel.mockRejectedValue(new Error("cancel failed"));
|
|
339
|
+
|
|
340
|
+
const streamId = "cancel-test-6";
|
|
341
|
+
const agentId = "agent-1" as AgentId;
|
|
342
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
343
|
+
|
|
344
|
+
// Should not throw even if session.cancel() fails
|
|
345
|
+
const result = await handler.processRequest(
|
|
346
|
+
agentId,
|
|
347
|
+
envelope(streamId, "session/cancel", { sessionId }, sessionId),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(result.acp.result).toEqual({ cancelled: true });
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should NOT remove the session mapping (agent stays alive)", async () => {
|
|
354
|
+
await setup([
|
|
355
|
+
{
|
|
356
|
+
sessionUpdate: "agent_message_chunk",
|
|
357
|
+
content: { type: "text", text: "Response after cancel" },
|
|
358
|
+
},
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
const streamId = "cancel-test-7";
|
|
362
|
+
const agentId = "agent-1" as AgentId;
|
|
363
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
364
|
+
|
|
365
|
+
// Cancel
|
|
366
|
+
await handler.processRequest(
|
|
367
|
+
agentId,
|
|
368
|
+
envelope(streamId, "session/cancel", { sessionId }, sessionId),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Subsequent prompt should still work (session mapping preserved)
|
|
372
|
+
const promptResult = await handler.processRequest(
|
|
373
|
+
agentId,
|
|
374
|
+
envelope(streamId, "session/prompt", {
|
|
375
|
+
prompt: [{ type: "text", text: "Hello again" }],
|
|
376
|
+
}, sessionId),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(promptResult.acp.error).toBeUndefined();
|
|
380
|
+
expect(promptResult.acp.result).toMatchObject({ stopReason: "end_turn" });
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ─────────────────────────────────────────────────────────────────
|
|
385
|
+
// abortStreamsForAgent
|
|
386
|
+
// ─────────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe("abortStreamsForAgent", () => {
|
|
389
|
+
it("should abort streams belonging to the given agent", async () => {
|
|
390
|
+
await setup([
|
|
391
|
+
{
|
|
392
|
+
sessionUpdate: "agent_message_chunk",
|
|
393
|
+
content: { type: "text", text: "Hello" },
|
|
394
|
+
},
|
|
395
|
+
]);
|
|
396
|
+
|
|
397
|
+
const streamId = "abort-test-1";
|
|
398
|
+
const agentId = "agent-1" as AgentId;
|
|
399
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
400
|
+
|
|
401
|
+
// Do a prompt to associate the stream with the agent
|
|
402
|
+
await handler.processRequest(
|
|
403
|
+
agentId,
|
|
404
|
+
envelope(streamId, "session/prompt", {
|
|
405
|
+
prompt: [{ type: "text", text: "Hi" }],
|
|
406
|
+
}, sessionId),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// Abort all streams for this agent
|
|
410
|
+
handler.abortStreamsForAgent(agentId);
|
|
411
|
+
|
|
412
|
+
// Verify by checking that a new prompt resets the abort controller
|
|
413
|
+
// (if it was aborted, handlePrompt will create a new controller)
|
|
414
|
+
const result = await handler.processRequest(
|
|
415
|
+
agentId,
|
|
416
|
+
envelope(streamId, "session/prompt", {
|
|
417
|
+
prompt: [{ type: "text", text: "After abort" }],
|
|
418
|
+
}, sessionId),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
expect(result.acp.error).toBeUndefined();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("should not affect streams for other agents", async () => {
|
|
425
|
+
await setup([
|
|
426
|
+
{
|
|
427
|
+
sessionUpdate: "agent_message_chunk",
|
|
428
|
+
content: { type: "text", text: "Hello" },
|
|
429
|
+
},
|
|
430
|
+
]);
|
|
431
|
+
|
|
432
|
+
const streamId = "abort-test-2";
|
|
433
|
+
const agentId = "agent-1" as AgentId;
|
|
434
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
435
|
+
|
|
436
|
+
// Do a prompt to associate the stream with the agent
|
|
437
|
+
await handler.processRequest(
|
|
438
|
+
agentId,
|
|
439
|
+
envelope(streamId, "session/prompt", {
|
|
440
|
+
prompt: [{ type: "text", text: "Hi" }],
|
|
441
|
+
}, sessionId),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Abort streams for a different agent — should NOT affect our stream
|
|
445
|
+
handler.abortStreamsForAgent("agent-other" as AgentId);
|
|
446
|
+
|
|
447
|
+
// Our stream should still work normally
|
|
448
|
+
const result = await handler.processRequest(
|
|
449
|
+
agentId,
|
|
450
|
+
envelope(streamId, "session/prompt", {
|
|
451
|
+
prompt: [{ type: "text", text: "Still working" }],
|
|
452
|
+
}, sessionId),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
expect(result.acp.error).toBeUndefined();
|
|
456
|
+
expect(result.acp.result).toMatchObject({ stopReason: "end_turn" });
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// ─────────────────────────────────────────────────────────────────
|
|
461
|
+
// closeStream
|
|
462
|
+
// ─────────────────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
describe("closeStream", () => {
|
|
465
|
+
it("should remove the stream and abort its controller", async () => {
|
|
466
|
+
await setup();
|
|
467
|
+
|
|
468
|
+
const streamId = "close-test-1";
|
|
469
|
+
const agentId = "agent-1" as AgentId;
|
|
470
|
+
await initAndCreateSession(streamId, agentId);
|
|
471
|
+
|
|
472
|
+
// Close the stream
|
|
473
|
+
handler.closeStream(streamId);
|
|
474
|
+
|
|
475
|
+
// Using the closed stream should create a new stream state
|
|
476
|
+
// (processRequest creates a new state if none exists)
|
|
477
|
+
const result = await handler.processRequest(
|
|
478
|
+
agentId,
|
|
479
|
+
envelope(streamId, "initialize", {
|
|
480
|
+
protocolVersion: 1,
|
|
481
|
+
capabilities: {},
|
|
482
|
+
clientInfo: { name: "test", version: "1.0" },
|
|
483
|
+
}),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Should succeed (new stream state created)
|
|
487
|
+
expect(result.acp.error).toBeUndefined();
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ─────────────────────────────────────────────────────────────────
|
|
492
|
+
// Cancel during active prompt (integration)
|
|
493
|
+
// ─────────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
describe("cancel during active prompt", () => {
|
|
496
|
+
it("should return stopReason:cancelled when abort controller fires during prompt", async () => {
|
|
497
|
+
// Create a prompt that yields updates slowly so we can cancel mid-stream
|
|
498
|
+
const { agentManager } = await setup([], { slowYield: 50 });
|
|
499
|
+
|
|
500
|
+
// Override prompt to yield many slow updates
|
|
501
|
+
let abortSignalRef: AbortSignal | undefined;
|
|
502
|
+
(agentManager.prompt as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
503
|
+
[Symbol.asyncIterator]: async function* () {
|
|
504
|
+
for (let i = 0; i < 100; i++) {
|
|
505
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
506
|
+
yield {
|
|
507
|
+
sessionUpdate: "agent_message_chunk",
|
|
508
|
+
content: { type: "text", text: `chunk-${i}` },
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const streamId = "cancel-during-prompt";
|
|
515
|
+
const agentId = "agent-1" as AgentId;
|
|
516
|
+
const sessionId = await initAndCreateSession(streamId, agentId);
|
|
517
|
+
|
|
518
|
+
// Start prompt and cancel concurrently
|
|
519
|
+
const promptPromise = handler.processRequest(
|
|
520
|
+
agentId,
|
|
521
|
+
envelope(streamId, "session/prompt", {
|
|
522
|
+
prompt: [{ type: "text", text: "Long running task" }],
|
|
523
|
+
}, sessionId),
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
// Wait a bit then cancel
|
|
527
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
528
|
+
await handler.processRequest(
|
|
529
|
+
agentId,
|
|
530
|
+
envelope(streamId, "session/cancel", { sessionId }, sessionId),
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const result = await promptPromise;
|
|
534
|
+
|
|
535
|
+
// Should have been cancelled
|
|
536
|
+
expect(result.acp.result).toMatchObject({ stopReason: "cancelled" });
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// ─────────────────────────────────────────────────────────────────
|
|
542
|
+
// MAP stop (map/agents/stop via handleStopAgent) — hard kill
|
|
543
|
+
// ─────────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
describe("MAPAdapter handleStopAgent (map/agents/stop)", () => {
|
|
546
|
+
let adapter: MAPAdapter;
|
|
547
|
+
let eventStore: EventStore;
|
|
548
|
+
let mockAgentManager: AgentManager;
|
|
549
|
+
let emittedEvents: EventNotification[];
|
|
550
|
+
|
|
551
|
+
async function setupAdapter(overrides?: Partial<AgentManager>) {
|
|
552
|
+
eventStore = await createEventStore({ inMemory: true });
|
|
553
|
+
emittedEvents = [];
|
|
554
|
+
|
|
555
|
+
const mockAgent = createMockAgent();
|
|
556
|
+
const mockSession = {
|
|
557
|
+
cancel: vi.fn().mockResolvedValue(undefined),
|
|
558
|
+
id: "session-1",
|
|
559
|
+
cwd: "/test/cwd",
|
|
560
|
+
modes: [],
|
|
561
|
+
models: [],
|
|
562
|
+
isProcessing: false,
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
mockAgentManager = {
|
|
566
|
+
spawn: vi.fn(),
|
|
567
|
+
get: vi.fn().mockReturnValue(mockAgent),
|
|
568
|
+
list: vi.fn().mockReturnValue([mockAgent]),
|
|
569
|
+
listHeadManagers: vi.fn().mockReturnValue([mockAgent]),
|
|
570
|
+
getChildren: vi.fn().mockReturnValue([]),
|
|
571
|
+
getHierarchy: vi.fn().mockReturnValue({
|
|
572
|
+
root: { agent: mockAgent, children: [] },
|
|
573
|
+
depth: 1,
|
|
574
|
+
totalAgents: 1,
|
|
575
|
+
}),
|
|
576
|
+
getOrCreateHeadManager: vi.fn(),
|
|
577
|
+
hasActiveSession: vi.fn().mockReturnValue(true),
|
|
578
|
+
resume: vi.fn(),
|
|
579
|
+
terminate: vi.fn().mockResolvedValue(undefined),
|
|
580
|
+
prompt: vi.fn().mockReturnValue({
|
|
581
|
+
[Symbol.asyncIterator]: async function* () {},
|
|
582
|
+
}),
|
|
583
|
+
getSession: vi.fn().mockReturnValue(mockSession),
|
|
584
|
+
onLifecycleEvent: vi.fn().mockReturnValue(() => {}),
|
|
585
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
586
|
+
respondToPermission: vi.fn().mockReturnValue(true),
|
|
587
|
+
cancelPermission: vi.fn().mockReturnValue(true),
|
|
588
|
+
...overrides,
|
|
589
|
+
} as unknown as AgentManager;
|
|
590
|
+
|
|
591
|
+
const mockTaskManager = createMockTaskManager();
|
|
592
|
+
|
|
593
|
+
const services: MAPAdapterServices = {
|
|
594
|
+
getAgent: vi.fn(),
|
|
595
|
+
listAgents: vi.fn().mockReturnValue([]),
|
|
596
|
+
sendMessage: vi.fn().mockResolvedValue({ delivered: [] }),
|
|
597
|
+
getAncestors: vi.fn().mockReturnValue([]),
|
|
598
|
+
getDescendants: vi.fn().mockReturnValue([]),
|
|
599
|
+
agentManager: mockAgentManager,
|
|
600
|
+
eventStore,
|
|
601
|
+
taskManager: mockTaskManager,
|
|
602
|
+
defaultCwd: "/test/cwd",
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
adapter = createMAPAdapter(
|
|
606
|
+
{
|
|
607
|
+
name: "test-stop",
|
|
608
|
+
version: "1.0.0",
|
|
609
|
+
defaultClientCapabilities: {
|
|
610
|
+
canQuery: true,
|
|
611
|
+
canSubscribe: true,
|
|
612
|
+
canMessage: true,
|
|
613
|
+
canStop: true,
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
services,
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
// Listen for emitted events
|
|
620
|
+
adapter.onEvent((e) => {
|
|
621
|
+
if ("eventId" in e) {
|
|
622
|
+
emittedEvents.push(e as unknown as EventNotification);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
await adapter.start();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
afterEach(async () => {
|
|
630
|
+
if (adapter?.isRunning()) {
|
|
631
|
+
await adapter.stop();
|
|
632
|
+
}
|
|
633
|
+
if (eventStore) {
|
|
634
|
+
await eventStore.close();
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("should call agentManager.terminate() with agentId and reason", async () => {
|
|
639
|
+
await setupAdapter();
|
|
640
|
+
|
|
641
|
+
const impl = adapter as unknown as {
|
|
642
|
+
handleStopAgent: (
|
|
643
|
+
participantId: ParticipantId,
|
|
644
|
+
params: unknown,
|
|
645
|
+
) => Promise<{ stopping: boolean }>;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const result = await impl.handleStopAgent(
|
|
649
|
+
"p-test" as ParticipantId,
|
|
650
|
+
{ agentId: "agent-1" as AgentId, reason: "user stopped" },
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
expect(result).toEqual({ stopping: true });
|
|
654
|
+
expect(mockAgentManager.terminate).toHaveBeenCalledWith(
|
|
655
|
+
"agent-1",
|
|
656
|
+
"user stopped",
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("should use 'cancelled' as default reason", async () => {
|
|
661
|
+
await setupAdapter();
|
|
662
|
+
|
|
663
|
+
const impl = adapter as unknown as {
|
|
664
|
+
handleStopAgent: (
|
|
665
|
+
participantId: ParticipantId,
|
|
666
|
+
params: unknown,
|
|
667
|
+
) => Promise<{ stopping: boolean }>;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
await impl.handleStopAgent(
|
|
671
|
+
"p-test" as ParticipantId,
|
|
672
|
+
{ agentId: "agent-1" as AgentId },
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
expect(mockAgentManager.terminate).toHaveBeenCalledWith(
|
|
676
|
+
"agent-1",
|
|
677
|
+
"cancelled",
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("should throw invalidParams when agentId is missing", async () => {
|
|
682
|
+
await setupAdapter();
|
|
683
|
+
|
|
684
|
+
const impl = adapter as unknown as {
|
|
685
|
+
handleStopAgent: (
|
|
686
|
+
participantId: ParticipantId,
|
|
687
|
+
params: unknown,
|
|
688
|
+
) => Promise<{ stopping: boolean }>;
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
await expect(
|
|
692
|
+
impl.handleStopAgent("p-test" as ParticipantId, {}),
|
|
693
|
+
).rejects.toThrow("agentId required");
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("should throw internalError when agentManager is not available", async () => {
|
|
697
|
+
// Create adapter without agentManager in services
|
|
698
|
+
eventStore = await createEventStore({ inMemory: true });
|
|
699
|
+
|
|
700
|
+
const services: MAPAdapterServices = {
|
|
701
|
+
getAgent: vi.fn(),
|
|
702
|
+
listAgents: vi.fn().mockReturnValue([]),
|
|
703
|
+
sendMessage: vi.fn().mockResolvedValue({ delivered: [] }),
|
|
704
|
+
getAncestors: vi.fn().mockReturnValue([]),
|
|
705
|
+
getDescendants: vi.fn().mockReturnValue([]),
|
|
706
|
+
// No agentManager!
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
adapter = createMAPAdapter({ name: "test-no-am" }, services);
|
|
710
|
+
await adapter.start();
|
|
711
|
+
|
|
712
|
+
const impl = adapter as unknown as {
|
|
713
|
+
handleStopAgent: (
|
|
714
|
+
participantId: ParticipantId,
|
|
715
|
+
params: unknown,
|
|
716
|
+
) => Promise<{ stopping: boolean }>;
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
await expect(
|
|
720
|
+
impl.handleStopAgent("p-test" as ParticipantId, {
|
|
721
|
+
agentId: "agent-1" as AgentId,
|
|
722
|
+
}),
|
|
723
|
+
).rejects.toThrow("Agent manager not available");
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("should throw internalError when terminate fails", async () => {
|
|
727
|
+
await setupAdapter({
|
|
728
|
+
terminate: vi.fn().mockRejectedValue(new Error("Agent not found")),
|
|
729
|
+
} as unknown as Partial<AgentManager>);
|
|
730
|
+
|
|
731
|
+
const impl = adapter as unknown as {
|
|
732
|
+
handleStopAgent: (
|
|
733
|
+
participantId: ParticipantId,
|
|
734
|
+
params: unknown,
|
|
735
|
+
) => Promise<{ stopping: boolean }>;
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
await expect(
|
|
739
|
+
impl.handleStopAgent("p-test" as ParticipantId, {
|
|
740
|
+
agentId: "agent-1" as AgentId,
|
|
741
|
+
}),
|
|
742
|
+
).rejects.toThrow("Failed to stop agent: Agent not found");
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("should emit agent.state.changed event via lifecycle listener", async () => {
|
|
746
|
+
// Capture lifecycle callback so we can fire it from the mock terminate
|
|
747
|
+
let lifecycleCallback: ((event: unknown) => void) | undefined;
|
|
748
|
+
|
|
749
|
+
await setupAdapter({
|
|
750
|
+
onLifecycleEvent: vi.fn().mockImplementation((cb: (event: unknown) => void) => {
|
|
751
|
+
lifecycleCallback = cb;
|
|
752
|
+
return () => {};
|
|
753
|
+
}),
|
|
754
|
+
terminate: vi.fn().mockImplementation(async () => {
|
|
755
|
+
// Simulate what real agentManager.terminate() does: fire lifecycle
|
|
756
|
+
if (lifecycleCallback) {
|
|
757
|
+
lifecycleCallback({
|
|
758
|
+
type: "stopped",
|
|
759
|
+
agent: { id: "agent-1", name: "test-agent", role: "worker", state: "stopped" },
|
|
760
|
+
reason: "user stopped",
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}),
|
|
764
|
+
} as unknown as Partial<AgentManager>);
|
|
765
|
+
|
|
766
|
+
const impl = adapter as unknown as {
|
|
767
|
+
handleStopAgent: (
|
|
768
|
+
participantId: ParticipantId,
|
|
769
|
+
params: unknown,
|
|
770
|
+
) => Promise<{ stopping: boolean }>;
|
|
771
|
+
emitEvent: (event: EventNotification) => void;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// Spy on emitEvent to capture what was emitted
|
|
775
|
+
const emitSpy = vi.spyOn(impl, "emitEvent");
|
|
776
|
+
|
|
777
|
+
await impl.handleStopAgent(
|
|
778
|
+
"p-test" as ParticipantId,
|
|
779
|
+
{ agentId: "agent-1" as AgentId, reason: "user stopped" },
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
// Verify emitEvent was called with the right event shape via lifecycle
|
|
783
|
+
expect(emitSpy).toHaveBeenCalledWith(
|
|
784
|
+
expect.objectContaining({
|
|
785
|
+
type: "agent_state_changed",
|
|
786
|
+
agentId: "agent-1",
|
|
787
|
+
data: expect.objectContaining({
|
|
788
|
+
agentId: "agent-1",
|
|
789
|
+
current: "stopped",
|
|
790
|
+
previous: "running",
|
|
791
|
+
reason: "user stopped",
|
|
792
|
+
}),
|
|
793
|
+
}),
|
|
794
|
+
);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("should abort ACP streams for the agent before terminating", async () => {
|
|
798
|
+
await setupAdapter();
|
|
799
|
+
|
|
800
|
+
// Access the internal ACP handler to spy on abortStreamsForAgent
|
|
801
|
+
const impl = adapter as unknown as {
|
|
802
|
+
acpOverMapHandler: ACPOverMAPHandler | null;
|
|
803
|
+
handleStopAgent: (
|
|
804
|
+
participantId: ParticipantId,
|
|
805
|
+
params: unknown,
|
|
806
|
+
) => Promise<{ stopping: boolean }>;
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const abortSpy = vi.spyOn(impl.acpOverMapHandler!, "abortStreamsForAgent");
|
|
810
|
+
|
|
811
|
+
await impl.handleStopAgent(
|
|
812
|
+
"p-test" as ParticipantId,
|
|
813
|
+
{ agentId: "agent-1" as AgentId },
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
expect(abortSpy).toHaveBeenCalledWith("agent-1");
|
|
817
|
+
// terminate should be called AFTER abort
|
|
818
|
+
expect(mockAgentManager.terminate).toHaveBeenCalled();
|
|
819
|
+
});
|
|
820
|
+
});
|