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,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E test: Spawned Agent Session Streaming
|
|
3
|
+
*
|
|
4
|
+
* Verifies that when an agent is spawned via the MCP bridge (`_macro/mcp/spawn_agent`),
|
|
5
|
+
* the fire-and-forget prompt emits MAP events that TUI clients can subscribe to:
|
|
6
|
+
* - session_user_message (before prompt starts)
|
|
7
|
+
* - session_update (for each streaming update during the prompt)
|
|
8
|
+
* - session_prompt_done (after the prompt completes)
|
|
9
|
+
*
|
|
10
|
+
* Also verifies that turns are recorded in EventStore for history persistence.
|
|
11
|
+
*
|
|
12
|
+
* REQUIRES: RUN_FULL_AGENT_TESTS=true environment variable (and authenticated Claude Code)
|
|
13
|
+
*
|
|
14
|
+
* Run with:
|
|
15
|
+
* RUN_FULL_AGENT_TESTS=true npx vitest run src/__tests__/e2e/spawn-session-streaming.e2e.test.ts
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
19
|
+
import { WebSocket } from "ws";
|
|
20
|
+
import * as fs from "fs";
|
|
21
|
+
import * as path from "path";
|
|
22
|
+
import * as os from "os";
|
|
23
|
+
import { createEventStore, type EventStore } from "../../store/event-store.js";
|
|
24
|
+
import {
|
|
25
|
+
createAgentManager,
|
|
26
|
+
type AgentManager,
|
|
27
|
+
} from "../../agent/agent-manager.js";
|
|
28
|
+
import { createTaskManager, type TaskManager } from "../../task/task-manager.js";
|
|
29
|
+
import {
|
|
30
|
+
createMessageRouter,
|
|
31
|
+
type MessageRouter,
|
|
32
|
+
} from "../../router/message-router.js";
|
|
33
|
+
import {
|
|
34
|
+
createCombinedServer,
|
|
35
|
+
type CombinedServer,
|
|
36
|
+
type CombinedServerServices,
|
|
37
|
+
} from "../../server/combined-server.js";
|
|
38
|
+
import { mapCall } from "../../mcp/map-client.js";
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Test Configuration
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
const RUN_FULL_AGENT = !!process.env.RUN_FULL_AGENT_TESTS;
|
|
45
|
+
const testFn = RUN_FULL_AGENT ? it : it.skip;
|
|
46
|
+
|
|
47
|
+
const TIMEOUT = {
|
|
48
|
+
SPAWN_AND_PROMPT: 120_000, // Agent spawn + prompt completion
|
|
49
|
+
EVENT_WAIT: 60_000, // Waiting for individual MAP events
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const log = (msg: string) => {
|
|
53
|
+
if (RUN_FULL_AGENT) {
|
|
54
|
+
console.log(`[SpawnStreamE2E] ${msg}`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// No event type filter needed — the TUI worker subscribes to ALL events
|
|
59
|
+
// by calling client.subscribe() without a filter. The e2e test mirrors this.
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Helpers
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
function getRandomPort(): number {
|
|
66
|
+
return 10000 + Math.floor(Math.random() * 50000);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface JsonRpcMessage {
|
|
70
|
+
jsonrpc: "2.0";
|
|
71
|
+
id?: number;
|
|
72
|
+
method?: string;
|
|
73
|
+
params?: unknown;
|
|
74
|
+
result?: unknown;
|
|
75
|
+
error?: { code: number; message: string; data?: unknown };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* WebSocket MAP client for subscribing to events.
|
|
80
|
+
*/
|
|
81
|
+
class MAPTestClient {
|
|
82
|
+
private ws!: WebSocket;
|
|
83
|
+
private waiters: Map<
|
|
84
|
+
number,
|
|
85
|
+
{ resolve: (r: JsonRpcMessage) => void; reject: (e: Error) => void }
|
|
86
|
+
> = new Map();
|
|
87
|
+
private nextId = 1;
|
|
88
|
+
private url: string;
|
|
89
|
+
|
|
90
|
+
readonly notifications: JsonRpcMessage[] = [];
|
|
91
|
+
|
|
92
|
+
private notificationWaiters: Array<{
|
|
93
|
+
check: (msg: JsonRpcMessage) => boolean;
|
|
94
|
+
resolve: (msg: JsonRpcMessage) => void;
|
|
95
|
+
reject: (e: Error) => void;
|
|
96
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
97
|
+
}> = [];
|
|
98
|
+
|
|
99
|
+
constructor(url: string) {
|
|
100
|
+
this.url = url;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async connect(): Promise<void> {
|
|
104
|
+
this.ws = new WebSocket(this.url);
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const timeout = setTimeout(
|
|
107
|
+
() => reject(new Error("Connection timeout")),
|
|
108
|
+
5000,
|
|
109
|
+
);
|
|
110
|
+
this.ws.on("open", () => {
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
this.ws.on("error", (err) => {
|
|
115
|
+
clearTimeout(timeout);
|
|
116
|
+
reject(err);
|
|
117
|
+
});
|
|
118
|
+
this.ws.on("message", (data: Buffer) => {
|
|
119
|
+
try {
|
|
120
|
+
const msg = JSON.parse(data.toString()) as JsonRpcMessage;
|
|
121
|
+
if (msg.id != null) {
|
|
122
|
+
const waiter = this.waiters.get(msg.id);
|
|
123
|
+
if (waiter) {
|
|
124
|
+
this.waiters.delete(msg.id);
|
|
125
|
+
waiter.resolve(msg);
|
|
126
|
+
}
|
|
127
|
+
} else if (msg.method) {
|
|
128
|
+
this.notifications.push(msg);
|
|
129
|
+
for (let i = this.notificationWaiters.length - 1; i >= 0; i--) {
|
|
130
|
+
const w = this.notificationWaiters[i];
|
|
131
|
+
if (w.check(msg)) {
|
|
132
|
+
clearTimeout(w.timeout);
|
|
133
|
+
this.notificationWaiters.splice(i, 1);
|
|
134
|
+
w.resolve(msg);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// ignore
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async request(method: string, params?: unknown): Promise<JsonRpcMessage> {
|
|
146
|
+
const id = this.nextId++;
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const timeout = setTimeout(() => {
|
|
149
|
+
this.waiters.delete(id);
|
|
150
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
151
|
+
}, 30000);
|
|
152
|
+
this.waiters.set(id, {
|
|
153
|
+
resolve: (r) => {
|
|
154
|
+
clearTimeout(timeout);
|
|
155
|
+
resolve(r);
|
|
156
|
+
},
|
|
157
|
+
reject: (e) => {
|
|
158
|
+
clearTimeout(timeout);
|
|
159
|
+
reject(e);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
this.ws.send(JSON.stringify({ jsonrpc: "2.0", method, params, id }));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
waitForNotification(
|
|
167
|
+
check: (msg: JsonRpcMessage) => boolean,
|
|
168
|
+
timeoutMs = 15000,
|
|
169
|
+
): Promise<JsonRpcMessage> {
|
|
170
|
+
const existing = this.notifications.find(check);
|
|
171
|
+
if (existing) return Promise.resolve(existing);
|
|
172
|
+
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const timeout = setTimeout(() => {
|
|
175
|
+
const idx = this.notificationWaiters.findIndex(
|
|
176
|
+
(w) => w.resolve === resolve,
|
|
177
|
+
);
|
|
178
|
+
if (idx >= 0) this.notificationWaiters.splice(idx, 1);
|
|
179
|
+
reject(
|
|
180
|
+
new Error(
|
|
181
|
+
`Timeout waiting for notification. Received ${this.notifications.length} notifications:\n` +
|
|
182
|
+
this.notifications
|
|
183
|
+
.map((n) => {
|
|
184
|
+
const p = n.params as Record<string, unknown> | undefined;
|
|
185
|
+
const evt = p?.event as Record<string, unknown> | undefined;
|
|
186
|
+
return ` ${n.method} → type=${evt?.type}, agentId=${(evt?.data as Record<string, unknown> | undefined)?.agentId}`;
|
|
187
|
+
})
|
|
188
|
+
.join("\n"),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
this.notificationWaiters.push({ check, resolve, reject, timeout });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Collect all notifications matching a predicate that have arrived so far.
|
|
198
|
+
*/
|
|
199
|
+
collectNotifications(check: (msg: JsonRpcMessage) => boolean): JsonRpcMessage[] {
|
|
200
|
+
return this.notifications.filter(check);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
close(): void {
|
|
204
|
+
for (const w of this.notificationWaiters) {
|
|
205
|
+
clearTimeout(w.timeout);
|
|
206
|
+
}
|
|
207
|
+
this.notificationWaiters.length = 0;
|
|
208
|
+
try {
|
|
209
|
+
this.ws?.close();
|
|
210
|
+
} catch {
|
|
211
|
+
// ignore
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isEventOfType(type: string) {
|
|
217
|
+
return (msg: JsonRpcMessage) => {
|
|
218
|
+
if (msg.method !== "map/event") return false;
|
|
219
|
+
const params = msg.params as Record<string, unknown> | undefined;
|
|
220
|
+
const event = params?.event as Record<string, unknown> | undefined;
|
|
221
|
+
return event?.type === type;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isEventForAgent(type: string, agentId: string) {
|
|
226
|
+
return (msg: JsonRpcMessage) => {
|
|
227
|
+
if (msg.method !== "map/event") return false;
|
|
228
|
+
const params = msg.params as Record<string, unknown> | undefined;
|
|
229
|
+
const event = params?.event as Record<string, unknown> | undefined;
|
|
230
|
+
if (event?.type !== type) return false;
|
|
231
|
+
const data = event?.data as Record<string, unknown> | undefined;
|
|
232
|
+
return data?.agentId === agentId;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getEventData(msg: JsonRpcMessage): Record<string, unknown> {
|
|
237
|
+
const params = msg.params as Record<string, unknown>;
|
|
238
|
+
const event = params.event as Record<string, unknown>;
|
|
239
|
+
return event.data as Record<string, unknown>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// Tests
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
246
|
+
describe("Spawned Agent Session Streaming E2E", () => {
|
|
247
|
+
let eventStore: EventStore;
|
|
248
|
+
let agentManager: AgentManager;
|
|
249
|
+
let taskManager: TaskManager;
|
|
250
|
+
let messageRouter: MessageRouter;
|
|
251
|
+
let server: CombinedServer;
|
|
252
|
+
let port: number;
|
|
253
|
+
let serverUrl: string;
|
|
254
|
+
let tmpDir: string;
|
|
255
|
+
const clients: MAPTestClient[] = [];
|
|
256
|
+
|
|
257
|
+
beforeEach(async () => {
|
|
258
|
+
if (!RUN_FULL_AGENT) return;
|
|
259
|
+
|
|
260
|
+
port = getRandomPort();
|
|
261
|
+
serverUrl = `http://localhost:${port}`;
|
|
262
|
+
|
|
263
|
+
// File-based EventStore so the Claude Code subprocess can access the same DB
|
|
264
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "spawn-stream-e2e-"));
|
|
265
|
+
const instanceId = `test-spawn-stream-${Date.now()}`;
|
|
266
|
+
log(`EventStore baseDir: ${tmpDir}, instanceId: ${instanceId}`);
|
|
267
|
+
|
|
268
|
+
eventStore = await createEventStore({ instanceId, baseDir: tmpDir });
|
|
269
|
+
messageRouter = createMessageRouter(eventStore);
|
|
270
|
+
agentManager = createAgentManager(eventStore, messageRouter, {
|
|
271
|
+
defaultPermissionMode: "auto-approve",
|
|
272
|
+
defaultCwd: process.cwd(),
|
|
273
|
+
});
|
|
274
|
+
taskManager = createTaskManager(eventStore);
|
|
275
|
+
|
|
276
|
+
const services: CombinedServerServices = {
|
|
277
|
+
eventStore,
|
|
278
|
+
agentManager,
|
|
279
|
+
taskManager,
|
|
280
|
+
messageRouter,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
server = createCombinedServer(services, { port, host: "localhost" });
|
|
284
|
+
await server.start();
|
|
285
|
+
log(`Server started on port ${port}`);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
afterEach(async () => {
|
|
289
|
+
if (!RUN_FULL_AGENT) return;
|
|
290
|
+
|
|
291
|
+
for (const c of clients) {
|
|
292
|
+
c.close();
|
|
293
|
+
}
|
|
294
|
+
clients.length = 0;
|
|
295
|
+
|
|
296
|
+
// Terminate any running agents
|
|
297
|
+
try {
|
|
298
|
+
for (const agent of agentManager.list()) {
|
|
299
|
+
if (agent.state === "running") {
|
|
300
|
+
try {
|
|
301
|
+
await agentManager.terminate(agent.id, "test_cleanup");
|
|
302
|
+
} catch { /* ignore */ }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch { /* ignore */ }
|
|
306
|
+
|
|
307
|
+
await server.stop().catch(() => {});
|
|
308
|
+
await agentManager.close();
|
|
309
|
+
await eventStore.close();
|
|
310
|
+
|
|
311
|
+
// Clean up temp dir
|
|
312
|
+
try {
|
|
313
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
314
|
+
} catch { /* ignore */ }
|
|
315
|
+
|
|
316
|
+
log("Cleanup complete");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
function createClient(): MAPTestClient {
|
|
320
|
+
const client = new MAPTestClient(`ws://localhost:${port}/map`);
|
|
321
|
+
clients.push(client);
|
|
322
|
+
return client;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─────────────────────────────────────────────────────────────────
|
|
326
|
+
// Core: MAP event streaming during spawned agent prompt
|
|
327
|
+
// ─────────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
testFn(
|
|
330
|
+
"subscriber receives session_user_message, session_update, and session_prompt_done when agent is spawned",
|
|
331
|
+
{ timeout: TIMEOUT.SPAWN_AND_PROMPT },
|
|
332
|
+
async () => {
|
|
333
|
+
// Step 1: Subscribe to ALL events (no filter) — same as the real TUI worker
|
|
334
|
+
const subscriber = createClient();
|
|
335
|
+
await subscriber.connect();
|
|
336
|
+
const subRes = await subscriber.request("map/subscribe", {});
|
|
337
|
+
expect(subRes.error).toBeUndefined();
|
|
338
|
+
log("Subscriber connected and subscribed (no filter)");
|
|
339
|
+
|
|
340
|
+
// Step 2: Seed a root agent (the "caller" that spawns children)
|
|
341
|
+
eventStore.emit({
|
|
342
|
+
type: "spawn",
|
|
343
|
+
source: { agent_id: "system" },
|
|
344
|
+
payload: {
|
|
345
|
+
agent_id: "agent_caller",
|
|
346
|
+
session_id: "sess_caller",
|
|
347
|
+
task: "Root caller agent",
|
|
348
|
+
task_id: "task_caller",
|
|
349
|
+
parent: null,
|
|
350
|
+
config: {},
|
|
351
|
+
cwd: process.cwd(),
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
log("Root agent seeded");
|
|
355
|
+
|
|
356
|
+
// Step 3: Spawn a real agent via the MCP bridge
|
|
357
|
+
// Give it a simple task so it completes quickly
|
|
358
|
+
let spawnResult: { agent_id: string; session_id: string; task_id: string };
|
|
359
|
+
try {
|
|
360
|
+
spawnResult = await mapCall<{
|
|
361
|
+
agent_id: string;
|
|
362
|
+
session_id: string;
|
|
363
|
+
task_id: string;
|
|
364
|
+
}>(serverUrl, "_macro/mcp/spawn_agent", {
|
|
365
|
+
context: {
|
|
366
|
+
agent_id: "agent_caller",
|
|
367
|
+
session_id: "sess_caller",
|
|
368
|
+
task_id: "task_caller",
|
|
369
|
+
lineage: [],
|
|
370
|
+
cwd: process.cwd(),
|
|
371
|
+
},
|
|
372
|
+
task: "Say exactly: 'Hello from spawned agent'. Do not use any tools. Just respond with that text.",
|
|
373
|
+
});
|
|
374
|
+
log(`Agent spawned: ${spawnResult.agent_id} (session: ${spawnResult.session_id})`);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
throw new Error(`spawn_agent failed: ${(err as Error).message}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const agentId = spawnResult.agent_id;
|
|
380
|
+
|
|
381
|
+
// Step 4: Wait for session_user_message
|
|
382
|
+
log("Waiting for session_user_message...");
|
|
383
|
+
const userMsg = await subscriber.waitForNotification(
|
|
384
|
+
isEventForAgent("session_user_message", agentId),
|
|
385
|
+
TIMEOUT.EVENT_WAIT,
|
|
386
|
+
);
|
|
387
|
+
const userMsgData = getEventData(userMsg);
|
|
388
|
+
log(`Got session_user_message: content=${JSON.stringify(userMsgData.content).slice(0, 80)}...`);
|
|
389
|
+
|
|
390
|
+
expect(userMsgData.agentId).toBe(agentId);
|
|
391
|
+
expect(userMsgData.sessionId).toBe(spawnResult.session_id);
|
|
392
|
+
expect(userMsgData.content).toContain("Hello from spawned agent");
|
|
393
|
+
|
|
394
|
+
// Step 5: Wait for session_prompt_done (agent should finish quickly)
|
|
395
|
+
log("Waiting for session_prompt_done...");
|
|
396
|
+
const promptDone = await subscriber.waitForNotification(
|
|
397
|
+
isEventForAgent("session_prompt_done", agentId),
|
|
398
|
+
TIMEOUT.EVENT_WAIT,
|
|
399
|
+
);
|
|
400
|
+
const promptDoneData = getEventData(promptDone);
|
|
401
|
+
log(`Got session_prompt_done: stopReason=${promptDoneData.stopReason}`);
|
|
402
|
+
|
|
403
|
+
expect(promptDoneData.agentId).toBe(agentId);
|
|
404
|
+
expect(promptDoneData.stopReason).toBe("end_turn");
|
|
405
|
+
|
|
406
|
+
// Step 6: Verify session_update events were received (at least one)
|
|
407
|
+
const sessionUpdates = subscriber.collectNotifications(
|
|
408
|
+
isEventForAgent("session_update", agentId),
|
|
409
|
+
);
|
|
410
|
+
log(`Got ${sessionUpdates.length} session_update events`);
|
|
411
|
+
expect(sessionUpdates.length).toBeGreaterThan(0);
|
|
412
|
+
|
|
413
|
+
// Verify at least one update contains an agent_message_chunk
|
|
414
|
+
const hasTextChunk = sessionUpdates.some((msg) => {
|
|
415
|
+
const data = getEventData(msg);
|
|
416
|
+
const update = data.update as Record<string, unknown> | undefined;
|
|
417
|
+
return update?.sessionUpdate === "agent_message_chunk";
|
|
418
|
+
});
|
|
419
|
+
log(`Has text chunk in updates: ${hasTextChunk}`);
|
|
420
|
+
expect(hasTextChunk).toBe(true);
|
|
421
|
+
|
|
422
|
+
// Step 7: Verify event ordering: user_message → updates → prompt_done
|
|
423
|
+
const allSessionEvents = subscriber.collectNotifications((msg) => {
|
|
424
|
+
if (msg.method !== "map/event") return false;
|
|
425
|
+
const params = msg.params as Record<string, unknown> | undefined;
|
|
426
|
+
const event = params?.event as Record<string, unknown> | undefined;
|
|
427
|
+
const data = event?.data as Record<string, unknown> | undefined;
|
|
428
|
+
if (data?.agentId !== agentId) return false;
|
|
429
|
+
const type = event?.type as string;
|
|
430
|
+
return type === "session_user_message" || type === "session_update" || type === "session_prompt_done";
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const eventTypes = allSessionEvents.map((msg) => {
|
|
434
|
+
const params = msg.params as Record<string, unknown>;
|
|
435
|
+
const event = params.event as Record<string, unknown>;
|
|
436
|
+
return event.type;
|
|
437
|
+
});
|
|
438
|
+
log(`Event order: ${eventTypes.join(" → ")}`);
|
|
439
|
+
|
|
440
|
+
// First event should be user_message, last should be prompt_done
|
|
441
|
+
expect(eventTypes[0]).toBe("session_user_message");
|
|
442
|
+
expect(eventTypes[eventTypes.length - 1]).toBe("session_prompt_done");
|
|
443
|
+
|
|
444
|
+
// Step 8: Verify turns were recorded in EventStore for history persistence
|
|
445
|
+
// Wait a moment for the async turn recording to complete
|
|
446
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
447
|
+
|
|
448
|
+
const turnEvents = eventStore.query({
|
|
449
|
+
type: "turn",
|
|
450
|
+
source_agent_id: agentId,
|
|
451
|
+
});
|
|
452
|
+
log(`Turn events recorded: ${turnEvents.length}`);
|
|
453
|
+
|
|
454
|
+
// Should have at least a user turn and an assistant turn
|
|
455
|
+
const userTurns = turnEvents.filter(
|
|
456
|
+
(e) => (e.payload as Record<string, unknown>).participant === "user",
|
|
457
|
+
);
|
|
458
|
+
const assistantTurns = turnEvents.filter(
|
|
459
|
+
(e) => (e.payload as Record<string, unknown>).participant === agentId,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
log(`User turns: ${userTurns.length}, Assistant turns: ${assistantTurns.length}`);
|
|
463
|
+
expect(userTurns.length).toBeGreaterThanOrEqual(1);
|
|
464
|
+
expect(assistantTurns.length).toBeGreaterThanOrEqual(1);
|
|
465
|
+
|
|
466
|
+
// Verify the user turn contains the task prompt
|
|
467
|
+
const userTurnPayload = userTurns[0].payload as Record<string, unknown>;
|
|
468
|
+
expect(userTurnPayload.content).toContain("Hello from spawned agent");
|
|
469
|
+
expect(userTurnPayload.conversation_id).toBe(spawnResult.session_id);
|
|
470
|
+
|
|
471
|
+
// Verify the assistant turn has content
|
|
472
|
+
const assistantPayload = assistantTurns[0].payload as Record<string, unknown>;
|
|
473
|
+
expect(assistantPayload.content_type).toBe("assistant_response");
|
|
474
|
+
const content = assistantPayload.content as { parts: Array<Record<string, unknown>> };
|
|
475
|
+
expect(content.parts.length).toBeGreaterThan(0);
|
|
476
|
+
expect(content.parts[0].type).toBe("text");
|
|
477
|
+
log(`Assistant response: ${JSON.stringify(content.parts[0].text).slice(0, 100)}...`);
|
|
478
|
+
|
|
479
|
+
log("All assertions passed!");
|
|
480
|
+
},
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
testFn(
|
|
484
|
+
"multiple subscribers each receive spawned agent session events",
|
|
485
|
+
{ timeout: TIMEOUT.SPAWN_AND_PROMPT },
|
|
486
|
+
async () => {
|
|
487
|
+
// Connect two subscribers
|
|
488
|
+
const sub1 = createClient();
|
|
489
|
+
const sub2 = createClient();
|
|
490
|
+
await sub1.connect();
|
|
491
|
+
await sub2.connect();
|
|
492
|
+
|
|
493
|
+
await sub1.request("map/subscribe", {});
|
|
494
|
+
await sub2.request("map/subscribe", {});
|
|
495
|
+
log("Two subscribers connected");
|
|
496
|
+
|
|
497
|
+
// Seed root agent
|
|
498
|
+
eventStore.emit({
|
|
499
|
+
type: "spawn",
|
|
500
|
+
source: { agent_id: "system" },
|
|
501
|
+
payload: {
|
|
502
|
+
agent_id: "agent_multi_caller",
|
|
503
|
+
session_id: "sess_multi_caller",
|
|
504
|
+
task: "Multi-subscriber test caller",
|
|
505
|
+
task_id: "task_multi_caller",
|
|
506
|
+
parent: null,
|
|
507
|
+
config: {},
|
|
508
|
+
cwd: process.cwd(),
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Spawn agent
|
|
513
|
+
const spawnResult = await mapCall<{
|
|
514
|
+
agent_id: string;
|
|
515
|
+
session_id: string;
|
|
516
|
+
task_id: string;
|
|
517
|
+
}>(serverUrl, "_macro/mcp/spawn_agent", {
|
|
518
|
+
context: {
|
|
519
|
+
agent_id: "agent_multi_caller",
|
|
520
|
+
session_id: "sess_multi_caller",
|
|
521
|
+
task_id: "task_multi_caller",
|
|
522
|
+
lineage: [],
|
|
523
|
+
cwd: process.cwd(),
|
|
524
|
+
},
|
|
525
|
+
task: "Say exactly: 'test'. Do not use any tools.",
|
|
526
|
+
});
|
|
527
|
+
log(`Agent spawned: ${spawnResult.agent_id}`);
|
|
528
|
+
|
|
529
|
+
const agentId = spawnResult.agent_id;
|
|
530
|
+
|
|
531
|
+
// Both subscribers should receive session_prompt_done
|
|
532
|
+
const [done1, done2] = await Promise.all([
|
|
533
|
+
sub1.waitForNotification(
|
|
534
|
+
isEventForAgent("session_prompt_done", agentId),
|
|
535
|
+
TIMEOUT.EVENT_WAIT,
|
|
536
|
+
),
|
|
537
|
+
sub2.waitForNotification(
|
|
538
|
+
isEventForAgent("session_prompt_done", agentId),
|
|
539
|
+
TIMEOUT.EVENT_WAIT,
|
|
540
|
+
),
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
expect(getEventData(done1).stopReason).toBe("end_turn");
|
|
544
|
+
expect(getEventData(done2).stopReason).toBe("end_turn");
|
|
545
|
+
|
|
546
|
+
// Both should have received session_user_message
|
|
547
|
+
const userMsg1 = sub1.collectNotifications(isEventForAgent("session_user_message", agentId));
|
|
548
|
+
const userMsg2 = sub2.collectNotifications(isEventForAgent("session_user_message", agentId));
|
|
549
|
+
expect(userMsg1.length).toBe(1);
|
|
550
|
+
expect(userMsg2.length).toBe(1);
|
|
551
|
+
|
|
552
|
+
// Both should have received session_update events
|
|
553
|
+
const updates1 = sub1.collectNotifications(isEventForAgent("session_update", agentId));
|
|
554
|
+
const updates2 = sub2.collectNotifications(isEventForAgent("session_update", agentId));
|
|
555
|
+
expect(updates1.length).toBeGreaterThan(0);
|
|
556
|
+
expect(updates2.length).toBeGreaterThan(0);
|
|
557
|
+
expect(updates1.length).toBe(updates2.length);
|
|
558
|
+
|
|
559
|
+
log(`Both subscribers received ${updates1.length} session_update events`);
|
|
560
|
+
log("Multi-subscriber test passed!");
|
|
561
|
+
},
|
|
562
|
+
);
|
|
563
|
+
});
|
|
@@ -228,7 +228,7 @@ describe("_macro/getHistory", () => {
|
|
|
228
228
|
title: "Read file",
|
|
229
229
|
status: "completed",
|
|
230
230
|
rawInput: { path: "/test.txt" },
|
|
231
|
-
|
|
231
|
+
rawOutput: "file contents here",
|
|
232
232
|
},
|
|
233
233
|
{
|
|
234
234
|
sessionUpdate: "agent_message_chunk",
|
|
@@ -258,10 +258,10 @@ describe("_macro/getHistory", () => {
|
|
|
258
258
|
const assistantContent = turns[1].content as {
|
|
259
259
|
parts: { type: string; text?: string; toolCallId?: string; title?: string; output?: unknown }[];
|
|
260
260
|
};
|
|
261
|
-
expect(assistantContent.parts).toHaveLength(
|
|
261
|
+
expect(assistantContent.parts).toHaveLength(3);
|
|
262
262
|
expect(assistantContent.parts[0]).toEqual({
|
|
263
263
|
type: "text",
|
|
264
|
-
text: "Let me check that.
|
|
264
|
+
text: "Let me check that.",
|
|
265
265
|
});
|
|
266
266
|
expect(assistantContent.parts[1]).toMatchObject({
|
|
267
267
|
type: "tool",
|
|
@@ -270,6 +270,10 @@ describe("_macro/getHistory", () => {
|
|
|
270
270
|
status: "completed",
|
|
271
271
|
output: "file contents here",
|
|
272
272
|
});
|
|
273
|
+
expect(assistantContent.parts[2]).toEqual({
|
|
274
|
+
type: "text",
|
|
275
|
+
text: " Done!",
|
|
276
|
+
});
|
|
273
277
|
});
|
|
274
278
|
|
|
275
279
|
it("should accumulate history across multiple prompts", async () => {
|
|
@@ -406,7 +410,7 @@ describe("_macro/getHistory", () => {
|
|
|
406
410
|
title: "Done tool",
|
|
407
411
|
status: "completed",
|
|
408
412
|
rawInput: { x: 1 },
|
|
409
|
-
|
|
413
|
+
rawOutput: "result",
|
|
410
414
|
},
|
|
411
415
|
]);
|
|
412
416
|
|