macro-agent 0.0.10 → 0.0.12
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/.macro-agent/teams/self-driving/prompts/grinder.md +27 -0
- package/.macro-agent/teams/self-driving/prompts/judge.md +27 -0
- package/.macro-agent/teams/self-driving/prompts/planner.md +33 -0
- package/.macro-agent/teams/self-driving/roles/grinder.yaml +17 -0
- package/.macro-agent/teams/self-driving/roles/judge.yaml +24 -0
- package/.macro-agent/teams/self-driving/roles/planner.yaml +18 -0
- package/.macro-agent/teams/self-driving/team.yaml +103 -0
- package/.macro-agent/teams/structured/prompts/developer.md +26 -0
- package/.macro-agent/teams/structured/prompts/lead.md +25 -0
- package/.macro-agent/teams/structured/prompts/reviewer.md +24 -0
- package/.macro-agent/teams/structured/roles/developer.yaml +12 -0
- package/.macro-agent/teams/structured/roles/lead.yaml +11 -0
- package/.macro-agent/teams/structured/roles/reviewer.yaml +19 -0
- package/.macro-agent/teams/structured/team.yaml +89 -0
- package/.sudocode/issues.jsonl +56 -51
- package/.sudocode/specs.jsonl +8 -1
- package/CLAUDE.md +121 -30
- package/README.md +60 -3
- package/dist/acp/macro-agent.d.ts +4 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +50 -4
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/session-mapper.d.ts +20 -1
- package/dist/acp/session-mapper.d.ts.map +1 -1
- package/dist/acp/session-mapper.js +90 -1
- package/dist/acp/session-mapper.js.map +1 -1
- package/dist/acp/types.d.ts +24 -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 +40 -1
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js +172 -8
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +22 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/agent/wake.d.ts +15 -0
- package/dist/agent/wake.d.ts.map +1 -1
- package/dist/agent/wake.js +15 -0
- package/dist/agent/wake.js.map +1 -1
- package/dist/agent-detection/command-builder.d.ts +30 -0
- package/dist/agent-detection/command-builder.d.ts.map +1 -0
- package/dist/agent-detection/command-builder.js +71 -0
- package/dist/agent-detection/command-builder.js.map +1 -0
- package/dist/agent-detection/detector.d.ts +84 -0
- package/dist/agent-detection/detector.d.ts.map +1 -0
- package/dist/agent-detection/detector.js +240 -0
- package/dist/agent-detection/detector.js.map +1 -0
- package/dist/agent-detection/index.d.ts +12 -0
- package/dist/agent-detection/index.d.ts.map +1 -0
- package/dist/agent-detection/index.js +14 -0
- package/dist/agent-detection/index.js.map +1 -0
- package/dist/agent-detection/registry.d.ts +53 -0
- package/dist/agent-detection/registry.d.ts.map +1 -0
- package/dist/agent-detection/registry.js +177 -0
- package/dist/agent-detection/registry.js.map +1 -0
- package/dist/agent-detection/types.d.ts +121 -0
- package/dist/agent-detection/types.d.ts.map +1 -0
- package/dist/agent-detection/types.js +20 -0
- package/dist/agent-detection/types.js.map +1 -0
- package/dist/api/server.d.ts +5 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +362 -0
- package/dist/api/server.js.map +1 -1
- package/dist/api/types.d.ts +50 -1
- package/dist/api/types.d.ts.map +1 -1
- package/dist/cli/acp.d.ts +2 -0
- package/dist/cli/acp.d.ts.map +1 -1
- package/dist/cli/acp.js +8 -1
- package/dist/cli/acp.js.map +1 -1
- package/dist/cli/index.js +29 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.js +38 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/project-config.d.ts +46 -0
- package/dist/config/project-config.d.ts.map +1 -0
- package/dist/config/project-config.js +68 -0
- package/dist/config/project-config.js.map +1 -0
- package/dist/lifecycle/cascade.d.ts +1 -1
- package/dist/lifecycle/cascade.d.ts.map +1 -1
- package/dist/lifecycle/handlers/index.d.ts +4 -0
- package/dist/lifecycle/handlers/index.d.ts.map +1 -1
- package/dist/lifecycle/handlers/index.js +2 -0
- package/dist/lifecycle/handlers/index.js.map +1 -1
- package/dist/lifecycle/handlers/worker.d.ts +4 -0
- package/dist/lifecycle/handlers/worker.d.ts.map +1 -1
- package/dist/lifecycle/handlers/worker.js +35 -3
- package/dist/lifecycle/handlers/worker.js.map +1 -1
- package/dist/mail/conversation-map.d.ts +33 -0
- package/dist/mail/conversation-map.d.ts.map +1 -0
- package/dist/mail/conversation-map.js +61 -0
- package/dist/mail/conversation-map.js.map +1 -0
- package/dist/mail/index.d.ts +11 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +11 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/mail-service.d.ts +85 -0
- package/dist/mail/mail-service.d.ts.map +1 -0
- package/dist/mail/mail-service.js +121 -0
- package/dist/mail/mail-service.js.map +1 -0
- package/dist/mail/stores/eventstore-conversation-store.d.ts +40 -0
- package/dist/mail/stores/eventstore-conversation-store.d.ts.map +1 -0
- package/dist/mail/stores/eventstore-conversation-store.js +131 -0
- package/dist/mail/stores/eventstore-conversation-store.js.map +1 -0
- package/dist/mail/stores/eventstore-participant-store.d.ts +43 -0
- package/dist/mail/stores/eventstore-participant-store.d.ts.map +1 -0
- package/dist/mail/stores/eventstore-participant-store.js +145 -0
- package/dist/mail/stores/eventstore-participant-store.js.map +1 -0
- package/dist/mail/stores/eventstore-thread-store.d.ts +46 -0
- package/dist/mail/stores/eventstore-thread-store.d.ts.map +1 -0
- package/dist/mail/stores/eventstore-thread-store.js +118 -0
- package/dist/mail/stores/eventstore-thread-store.js.map +1 -0
- package/dist/mail/stores/eventstore-turn-store.d.ts +47 -0
- package/dist/mail/stores/eventstore-turn-store.d.ts.map +1 -0
- package/dist/mail/stores/eventstore-turn-store.js +153 -0
- package/dist/mail/stores/eventstore-turn-store.js.map +1 -0
- package/dist/mail/stores/index.d.ts +12 -0
- package/dist/mail/stores/index.d.ts.map +1 -0
- package/dist/mail/stores/index.js +12 -0
- package/dist/mail/stores/index.js.map +1 -0
- package/dist/mail/stores/types.d.ts +146 -0
- package/dist/mail/stores/types.d.ts.map +1 -0
- package/dist/mail/stores/types.js +13 -0
- package/dist/mail/stores/types.js.map +1 -0
- package/dist/mail/turn-recorder.d.ts +30 -0
- package/dist/mail/turn-recorder.d.ts.map +1 -0
- package/dist/mail/turn-recorder.js +98 -0
- package/dist/mail/turn-recorder.js.map +1 -0
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
- package/dist/map/adapter/acp-over-map.js +32 -2
- package/dist/map/adapter/acp-over-map.js.map +1 -1
- package/dist/map/adapter/event-translator.d.ts.map +1 -1
- package/dist/map/adapter/event-translator.js +4 -0
- package/dist/map/adapter/event-translator.js.map +1 -1
- package/dist/map/adapter/extensions/agent-detection.d.ts +49 -0
- package/dist/map/adapter/extensions/agent-detection.d.ts.map +1 -0
- package/dist/map/adapter/extensions/agent-detection.js +91 -0
- package/dist/map/adapter/extensions/agent-detection.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 +39 -0
- package/dist/map/adapter/extensions/index.js.map +1 -1
- package/dist/map/adapter/extensions/resume.d.ts +47 -0
- package/dist/map/adapter/extensions/resume.d.ts.map +1 -0
- package/dist/map/adapter/extensions/resume.js +59 -0
- package/dist/map/adapter/extensions/resume.js.map +1 -0
- package/dist/map/adapter/extensions/workspace-files.d.ts +42 -0
- package/dist/map/adapter/extensions/workspace-files.d.ts.map +1 -0
- package/dist/map/adapter/extensions/workspace-files.js +338 -0
- package/dist/map/adapter/extensions/workspace-files.js.map +1 -0
- package/dist/map/adapter/mail-handler-adapter.d.ts +27 -0
- package/dist/map/adapter/mail-handler-adapter.d.ts.map +1 -0
- package/dist/map/adapter/mail-handler-adapter.js +292 -0
- package/dist/map/adapter/mail-handler-adapter.js.map +1 -0
- package/dist/map/adapter/map-adapter.d.ts +34 -10
- package/dist/map/adapter/map-adapter.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.js +110 -14
- package/dist/map/adapter/map-adapter.js.map +1 -1
- package/dist/map/adapter/rpc-handler.d.ts +4 -1
- package/dist/map/adapter/rpc-handler.d.ts.map +1 -1
- package/dist/map/adapter/rpc-handler.js +6 -0
- package/dist/map/adapter/rpc-handler.js.map +1 -1
- package/dist/map/index.d.ts +1 -0
- package/dist/map/index.d.ts.map +1 -1
- package/dist/map/index.js +2 -0
- package/dist/map/index.js.map +1 -1
- package/dist/map/types.d.ts +3 -1
- package/dist/map/types.d.ts.map +1 -1
- package/dist/map/types.js.map +1 -1
- package/dist/mcp/mcp-server.d.ts +6 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +45 -0
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/mcp/tools/claim_task.d.ts +35 -0
- package/dist/mcp/tools/claim_task.d.ts.map +1 -0
- package/dist/mcp/tools/claim_task.js +58 -0
- package/dist/mcp/tools/claim_task.js.map +1 -0
- package/dist/mcp/tools/done.d.ts +15 -2
- package/dist/mcp/tools/done.d.ts.map +1 -1
- package/dist/mcp/tools/done.js +45 -10
- package/dist/mcp/tools/done.js.map +1 -1
- package/dist/mcp/tools/list_claimable_tasks.d.ts +38 -0
- package/dist/mcp/tools/list_claimable_tasks.d.ts.map +1 -0
- package/dist/mcp/tools/list_claimable_tasks.js +63 -0
- package/dist/mcp/tools/list_claimable_tasks.js.map +1 -0
- package/dist/mcp/tools/unclaim_task.d.ts +31 -0
- package/dist/mcp/tools/unclaim_task.d.ts.map +1 -0
- package/dist/mcp/tools/unclaim_task.js +47 -0
- package/dist/mcp/tools/unclaim_task.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +2 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/metrics.d.ts +79 -0
- package/dist/metrics/metrics.d.ts.map +1 -0
- package/dist/metrics/metrics.js +166 -0
- package/dist/metrics/metrics.js.map +1 -0
- package/dist/roles/capabilities.d.ts +1 -0
- package/dist/roles/capabilities.d.ts.map +1 -1
- package/dist/roles/capabilities.js +3 -0
- package/dist/roles/capabilities.js.map +1 -1
- package/dist/roles/types.d.ts +1 -1
- package/dist/roles/types.d.ts.map +1 -1
- package/dist/router/channels.d.ts +2 -4
- package/dist/router/channels.d.ts.map +1 -1
- package/dist/router/channels.js.map +1 -1
- package/dist/router/message-router.d.ts +85 -9
- package/dist/router/message-router.d.ts.map +1 -1
- package/dist/router/message-router.js +203 -14
- package/dist/router/message-router.js.map +1 -1
- package/dist/router/role-resolver.d.ts +10 -1
- package/dist/router/role-resolver.d.ts.map +1 -1
- package/dist/router/role-resolver.js +15 -1
- package/dist/router/role-resolver.js.map +1 -1
- package/dist/router/types.d.ts +30 -1
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/types.js.map +1 -1
- package/dist/server/combined-server.d.ts +6 -0
- package/dist/server/combined-server.d.ts.map +1 -1
- package/dist/server/combined-server.js +24 -2
- package/dist/server/combined-server.js.map +1 -1
- package/dist/store/event-store.d.ts +14 -1
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +456 -4
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/types/agents.d.ts +1 -1
- package/dist/store/types/agents.d.ts.map +1 -1
- package/dist/store/types/conversations.d.ts +91 -0
- package/dist/store/types/conversations.d.ts.map +1 -0
- package/dist/store/types/conversations.js +8 -0
- package/dist/store/types/conversations.js.map +1 -0
- package/dist/store/types/events.d.ts +1 -1
- package/dist/store/types/events.d.ts.map +1 -1
- package/dist/store/types/events.js.map +1 -1
- package/dist/store/types/index.d.ts +2 -0
- package/dist/store/types/index.d.ts.map +1 -1
- package/dist/store/types/index.js +2 -0
- package/dist/store/types/index.js.map +1 -1
- package/dist/store/types/sessions.d.ts +44 -0
- package/dist/store/types/sessions.d.ts.map +1 -0
- package/dist/store/types/sessions.js +9 -0
- package/dist/store/types/sessions.js.map +1 -0
- package/dist/store/types/tasks.d.ts +2 -0
- package/dist/store/types/tasks.d.ts.map +1 -1
- package/dist/task/backend/memory.d.ts +4 -1
- package/dist/task/backend/memory.d.ts.map +1 -1
- package/dist/task/backend/memory.js +81 -0
- package/dist/task/backend/memory.js.map +1 -1
- package/dist/task/backend/types.d.ts +30 -0
- package/dist/task/backend/types.d.ts.map +1 -1
- package/dist/task/backend/types.js.map +1 -1
- package/dist/teams/index.d.ts +4 -0
- package/dist/teams/index.d.ts.map +1 -0
- package/dist/teams/index.js +4 -0
- package/dist/teams/index.js.map +1 -0
- package/dist/teams/team-loader.d.ts +20 -0
- package/dist/teams/team-loader.d.ts.map +1 -0
- package/dist/teams/team-loader.js +293 -0
- package/dist/teams/team-loader.js.map +1 -0
- package/dist/teams/team-runtime.d.ts +139 -0
- package/dist/teams/team-runtime.d.ts.map +1 -0
- package/dist/teams/team-runtime.js +613 -0
- package/dist/teams/team-runtime.js.map +1 -0
- package/dist/teams/types.d.ts +266 -0
- package/dist/teams/types.d.ts.map +1 -0
- package/dist/teams/types.js +20 -0
- package/dist/teams/types.js.map +1 -0
- package/dist/trigger/router/trigger-router.d.ts +30 -3
- package/dist/trigger/router/trigger-router.d.ts.map +1 -1
- package/dist/trigger/router/trigger-router.js +30 -3
- package/dist/trigger/router/trigger-router.js.map +1 -1
- package/dist/trigger/wake/types.d.ts +31 -5
- package/dist/trigger/wake/types.d.ts.map +1 -1
- package/dist/trigger/wake/types.js +19 -0
- package/dist/trigger/wake/types.js.map +1 -1
- package/dist/workspace/dataplane-adapter.d.ts +1 -1
- package/dist/workspace/dataplane-adapter.d.ts.map +1 -1
- package/dist/workspace/dataplane-adapter.js +1 -1
- package/dist/workspace/dataplane-adapter.js.map +1 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/strategies/index.d.ts +6 -0
- package/dist/workspace/strategies/index.d.ts.map +1 -0
- package/dist/workspace/strategies/index.js +5 -0
- package/dist/workspace/strategies/index.js.map +1 -0
- package/dist/workspace/strategies/optimistic.d.ts +26 -0
- package/dist/workspace/strategies/optimistic.d.ts.map +1 -0
- package/dist/workspace/strategies/optimistic.js +121 -0
- package/dist/workspace/strategies/optimistic.js.map +1 -0
- package/dist/workspace/strategies/queue.d.ts +26 -0
- package/dist/workspace/strategies/queue.d.ts.map +1 -0
- package/dist/workspace/strategies/queue.js +67 -0
- package/dist/workspace/strategies/queue.js.map +1 -0
- package/dist/workspace/strategies/registry.d.ts +37 -0
- package/dist/workspace/strategies/registry.d.ts.map +1 -0
- package/dist/workspace/strategies/registry.js +63 -0
- package/dist/workspace/strategies/registry.js.map +1 -0
- package/dist/workspace/strategies/trunk.d.ts +20 -0
- package/dist/workspace/strategies/trunk.d.ts.map +1 -0
- package/dist/workspace/strategies/trunk.js +108 -0
- package/dist/workspace/strategies/trunk.js.map +1 -0
- package/dist/workspace/strategies/types.d.ts +104 -0
- package/dist/workspace/strategies/types.d.ts.map +1 -0
- package/dist/workspace/strategies/types.js +11 -0
- package/dist/workspace/strategies/types.js.map +1 -0
- package/dist/workspace/types.d.ts +1 -1
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +1 -1
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/docs/implementation-details.md +1127 -0
- package/docs/implementation-summary.md +448 -0
- package/docs/mail-integration.md +608 -0
- package/docs/plan-self-driving-support.md +433 -0
- package/docs/spec-self-driving-support.md +462 -0
- package/docs/team-templates.md +860 -0
- package/docs/teams.md +233 -0
- package/package.json +5 -3
- package/src/acp/__tests__/integration.test.ts +161 -1
- package/src/acp/__tests__/macro-agent.test.ts +95 -0
- package/src/acp/__tests__/session-persistence.test.ts +276 -0
- package/src/acp/macro-agent.ts +79 -7
- package/src/acp/session-mapper.ts +108 -1
- package/src/acp/types.ts +33 -1
- package/src/agent/agent-manager.ts +278 -6
- package/src/agent/types.ts +27 -0
- package/src/agent/wake.ts +15 -0
- package/src/agent-detection/__tests__/command-builder.test.ts +336 -0
- package/src/agent-detection/__tests__/detector.test.ts +768 -0
- package/src/agent-detection/__tests__/registry.test.ts +254 -0
- package/src/agent-detection/command-builder.ts +90 -0
- package/src/agent-detection/detector.ts +307 -0
- package/src/agent-detection/index.ts +36 -0
- package/src/agent-detection/registry.ts +200 -0
- package/src/agent-detection/types.ts +184 -0
- package/src/api/__tests__/conversation-api.test.ts +468 -0
- package/src/api/server.ts +425 -1
- package/src/api/types.ts +64 -1
- package/src/cli/acp.ts +9 -1
- package/src/cli/index.ts +44 -0
- package/src/cli/mcp.ts +47 -0
- package/src/config/index.ts +9 -0
- package/src/config/project-config.ts +107 -0
- package/src/lifecycle/cascade.ts +1 -1
- package/src/lifecycle/handlers/index.ts +8 -0
- package/src/lifecycle/handlers/worker.ts +48 -3
- package/src/mail/__tests__/conversation-lifecycle.test.ts +409 -0
- package/src/mail/__tests__/eventstore-stores.test.ts +1073 -0
- package/src/mail/__tests__/mail-full-agent.e2e.test.ts +575 -0
- package/src/mail/__tests__/mail-integration.test.ts +759 -0
- package/src/mail/__tests__/mail-map-protocol.e2e.test.ts +1068 -0
- package/src/mail/__tests__/mail-service.test.ts +506 -0
- package/src/mail/__tests__/turn-recorder.test.ts +328 -0
- package/src/mail/conversation-map.ts +107 -0
- package/src/mail/index.ts +25 -0
- package/src/mail/mail-service.ts +257 -0
- package/src/mail/stores/eventstore-conversation-store.ts +146 -0
- package/src/mail/stores/eventstore-participant-store.ts +172 -0
- package/src/mail/stores/eventstore-thread-store.ts +129 -0
- package/src/mail/stores/eventstore-turn-store.ts +173 -0
- package/src/mail/stores/index.ts +12 -0
- package/src/mail/stores/types.ts +160 -0
- package/src/mail/turn-recorder.ts +124 -0
- package/src/map/README.md +79 -0
- package/src/map/adapter/__tests__/extensions.test.ts +359 -0
- package/src/map/adapter/__tests__/map-adapter.test.ts +90 -0
- package/src/map/adapter/__tests__/workspace-files.test.ts +673 -0
- package/src/map/adapter/acp-over-map.ts +45 -2
- package/src/map/adapter/event-translator.ts +4 -0
- package/src/map/adapter/extensions/agent-detection.ts +201 -0
- package/src/map/adapter/extensions/index.ts +63 -0
- package/src/map/adapter/extensions/resume.ts +114 -0
- package/src/map/adapter/extensions/workspace-files.ts +449 -0
- package/src/map/adapter/mail-handler-adapter.ts +429 -0
- package/src/map/adapter/map-adapter.ts +173 -27
- package/src/map/adapter/rpc-handler.ts +8 -1
- package/src/map/index.ts +3 -0
- package/src/map/types.ts +3 -1
- package/src/mcp/mcp-server.ts +67 -0
- package/src/mcp/tools/claim_task.ts +86 -0
- package/src/mcp/tools/done.ts +59 -10
- package/src/mcp/tools/list_claimable_tasks.ts +93 -0
- package/src/mcp/tools/unclaim_task.ts +71 -0
- package/src/metrics/index.ts +9 -0
- package/src/metrics/metrics.ts +280 -0
- package/src/roles/capabilities.ts +3 -0
- package/src/roles/types.ts +2 -1
- package/src/router/README.md +120 -0
- package/src/router/__tests__/message-router.test.ts +561 -0
- package/src/router/channels.ts +3 -4
- package/src/router/message-router.ts +308 -22
- package/src/router/role-resolver.ts +22 -1
- package/src/router/types.ts +36 -1
- package/src/server/combined-server.ts +36 -2
- package/src/store/README.md +134 -0
- package/src/store/event-store.ts +546 -3
- package/src/store/types/agents.ts +1 -1
- package/src/store/types/conversations.ts +129 -0
- package/src/store/types/events.ts +5 -1
- package/src/store/types/index.ts +2 -0
- package/src/store/types/sessions.ts +53 -0
- package/src/store/types/tasks.ts +3 -0
- package/src/task/backend/memory.ts +116 -0
- package/src/task/backend/types.ts +43 -0
- package/src/teams/__tests__/cross-subsystem.integration.test.ts +983 -0
- package/src/teams/__tests__/e2e/team-runtime.e2e.test.ts +553 -0
- package/src/teams/__tests__/team-system.test.ts +1280 -0
- package/src/teams/index.ts +13 -0
- package/src/teams/team-loader.ts +434 -0
- package/src/teams/team-runtime.ts +727 -0
- package/src/teams/types.ts +377 -0
- package/src/trigger/router/trigger-router.ts +30 -3
- package/src/trigger/wake/types.ts +32 -5
- package/src/trigger/wake/wake-manager.ts +2 -2
- package/src/workspace/dataplane-adapter.ts +1 -1
- package/src/workspace/index.ts +1 -1
- package/src/workspace/strategies/index.ts +18 -0
- package/src/workspace/strategies/optimistic.ts +136 -0
- package/src/workspace/strategies/queue.ts +81 -0
- package/src/workspace/strategies/registry.ts +89 -0
- package/src/workspace/strategies/trunk.ts +123 -0
- package/src/workspace/strategies/types.ts +145 -0
- package/src/workspace/types.ts +1 -1
- package/src/workspace/workspace-manager.ts +1 -1
- package/.claude/settings.local.json +0 -59
- package/dist/map/utils/address-translation.d.ts +0 -99
- package/dist/map/utils/address-translation.d.ts.map +0 -1
- package/dist/map/utils/address-translation.js +0 -285
- package/dist/map/utils/address-translation.js.map +0 -1
- package/dist/map/utils/index.d.ts +0 -7
- package/dist/map/utils/index.d.ts.map +0 -1
- package/dist/map/utils/index.js +0 -7
- package/dist/map/utils/index.js.map +0 -1
- package/openspec/AGENTS.md +0 -456
- package/openspec/changes/archive/2025-12-21-add-mvp-foundation/design.md +0 -128
- package/openspec/changes/archive/2025-12-21-add-mvp-foundation/proposal.md +0 -49
- package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/agent-manager/spec.md +0 -150
- package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/cli-api/spec.md +0 -258
- package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/event-store/spec.md +0 -160
- package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/mcp-tools/spec.md +0 -224
- package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/message-router/spec.md +0 -153
- package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/task-manager/spec.md +0 -136
- package/openspec/changes/archive/2025-12-21-add-mvp-foundation/tasks.md +0 -147
- package/openspec/project.md +0 -31
- package/openspec/specs/agent-manager/spec.md +0 -154
- package/openspec/specs/cli-api/spec.md +0 -262
- package/openspec/specs/event-store/spec.md +0 -164
- package/openspec/specs/mcp-tools/spec.md +0 -228
- package/openspec/specs/message-router/spec.md +0 -157
- package/openspec/specs/task-manager/spec.md +0 -140
- package/references/acp-factory-ref/CHANGELOG.md +0 -33
- package/references/acp-factory-ref/LICENSE +0 -21
- package/references/acp-factory-ref/README.md +0 -341
- package/references/acp-factory-ref/package-lock.json +0 -3102
- package/references/acp-factory-ref/package.json +0 -96
- package/references/acp-factory-ref/python/CHANGELOG.md +0 -33
- package/references/acp-factory-ref/python/LICENSE +0 -21
- package/references/acp-factory-ref/python/Makefile +0 -57
- package/references/acp-factory-ref/python/README.md +0 -253
- package/references/acp-factory-ref/python/pyproject.toml +0 -73
- package/references/acp-factory-ref/python/tests/__init__.py +0 -0
- package/references/acp-factory-ref/python/tests/e2e/__init__.py +0 -1
- package/references/acp-factory-ref/python/tests/e2e/test_codex_e2e.py +0 -349
- package/references/acp-factory-ref/python/tests/e2e/test_gemini_e2e.py +0 -165
- package/references/acp-factory-ref/python/tests/e2e/test_opencode_e2e.py +0 -296
- package/references/acp-factory-ref/python/tests/test_client_handler.py +0 -543
- package/references/acp-factory-ref/python/tests/test_pushable.py +0 -199
- package/references/claude-code-acp/.github/workflows/ci.yml +0 -45
- package/references/claude-code-acp/.github/workflows/publish.yml +0 -34
- package/references/claude-code-acp/.prettierrc.json +0 -4
- package/references/claude-code-acp/CHANGELOG.md +0 -249
- package/references/claude-code-acp/LICENSE +0 -222
- package/references/claude-code-acp/README.md +0 -53
- package/references/claude-code-acp/docs/RELEASES.md +0 -24
- package/references/claude-code-acp/eslint.config.js +0 -48
- package/references/claude-code-acp/package-lock.json +0 -4570
- package/references/claude-code-acp/package.json +0 -88
- package/references/claude-code-acp/scripts/release.sh +0 -119
- package/references/claude-code-acp/src/acp-agent.ts +0 -2065
- package/references/claude-code-acp/src/index.ts +0 -26
- package/references/claude-code-acp/src/lib.ts +0 -38
- package/references/claude-code-acp/src/mcp-server.ts +0 -911
- package/references/claude-code-acp/src/settings.ts +0 -522
- package/references/claude-code-acp/src/tests/.claude/commands/quick-math.md +0 -5
- package/references/claude-code-acp/src/tests/.claude/commands/say-hello.md +0 -6
- package/references/claude-code-acp/src/tests/acp-agent-fork.test.ts +0 -479
- package/references/claude-code-acp/src/tests/acp-agent.test.ts +0 -1502
- package/references/claude-code-acp/src/tests/extract-lines.test.ts +0 -103
- package/references/claude-code-acp/src/tests/fork-session.test.ts +0 -335
- package/references/claude-code-acp/src/tests/replace-and-calculate-location.test.ts +0 -334
- package/references/claude-code-acp/src/tests/settings.test.ts +0 -617
- package/references/claude-code-acp/src/tests/skills-options.test.ts +0 -187
- package/references/claude-code-acp/src/tests/tools.test.ts +0 -318
- package/references/claude-code-acp/src/tests/typescript-declarations.test.ts +0 -558
- package/references/claude-code-acp/src/tools.ts +0 -819
- package/references/claude-code-acp/src/utils.ts +0 -171
- package/references/claude-code-acp/tsconfig.json +0 -18
- package/references/claude-code-acp/vitest.config.ts +0 -19
- package/references/multi-agent-protocol/.sudocode/issues.jsonl +0 -82
- package/references/multi-agent-protocol/.sudocode/specs.jsonl +0 -9
- package/references/multi-agent-protocol/LICENSE +0 -21
- package/references/multi-agent-protocol/README.md +0 -113
- package/references/multi-agent-protocol/docs/00-design-specification.md +0 -460
- package/references/multi-agent-protocol/docs/01-open-questions.md +0 -1050
- package/references/multi-agent-protocol/docs/02-wire-protocol.md +0 -296
- package/references/multi-agent-protocol/docs/03-streaming-semantics.md +0 -252
- package/references/multi-agent-protocol/docs/04-error-handling.md +0 -231
- package/references/multi-agent-protocol/docs/05-connection-model.md +0 -244
- package/references/multi-agent-protocol/docs/06-visibility-permissions.md +0 -243
- package/references/multi-agent-protocol/docs/07-federation.md +0 -259
- package/references/multi-agent-protocol/docs/08-macro-agent-migration.md +0 -253
- package/references/multi-agent-protocol/package-lock.json +0 -3239
- package/references/multi-agent-protocol/package.json +0 -56
- package/references/multi-agent-protocol/schema/meta.json +0 -337
- package/references/multi-agent-protocol/schema/schema.json +0 -1828
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail MAP Protocol E2E Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests MAP mail/* protocol methods via real WebSocket connections,
|
|
5
|
+
* REST API endpoints, and WebSocket subscription channels.
|
|
6
|
+
*
|
|
7
|
+
* Uses a real CombinedServer with in-memory EventStore.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { WebSocket } from "ws";
|
|
12
|
+
import { createEventStore, type EventStore } from "../../store/event-store.js";
|
|
13
|
+
import {
|
|
14
|
+
createAgentManager,
|
|
15
|
+
type AgentManager,
|
|
16
|
+
} from "../../agent/agent-manager.js";
|
|
17
|
+
import { createTaskManager, type TaskManager } from "../../task/task-manager.js";
|
|
18
|
+
import {
|
|
19
|
+
createMessageRouter,
|
|
20
|
+
type MessageRouter,
|
|
21
|
+
} from "../../router/message-router.js";
|
|
22
|
+
import {
|
|
23
|
+
createCombinedServer,
|
|
24
|
+
type CombinedServer,
|
|
25
|
+
type CombinedServerServices,
|
|
26
|
+
} from "../../server/combined-server.js";
|
|
27
|
+
import type { AgentId } from "../../store/types/index.js";
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────
|
|
30
|
+
// Helpers
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function getRandomPort(): number {
|
|
34
|
+
return 10000 + Math.floor(Math.random() * 50000);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface JsonRpcResponse {
|
|
38
|
+
jsonrpc: "2.0";
|
|
39
|
+
id: number;
|
|
40
|
+
result?: any;
|
|
41
|
+
error?: { code: number; message: string; data?: unknown };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* JSON-RPC WebSocket client for MAP protocol at /map.
|
|
46
|
+
*/
|
|
47
|
+
class TestMAPClient {
|
|
48
|
+
private ws!: WebSocket;
|
|
49
|
+
private waiters: Map<number, { resolve: (r: JsonRpcResponse) => void; reject: (e: Error) => void }> = new Map();
|
|
50
|
+
private nextId = 1;
|
|
51
|
+
private url: string;
|
|
52
|
+
|
|
53
|
+
constructor(url: string) {
|
|
54
|
+
this.url = url;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async connect(): Promise<void> {
|
|
58
|
+
this.ws = new WebSocket(this.url);
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const timeout = setTimeout(() => reject(new Error("MAP connection timeout")), 5000);
|
|
61
|
+
this.ws.on("open", () => {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
resolve();
|
|
64
|
+
});
|
|
65
|
+
this.ws.on("error", (err) => {
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
reject(err);
|
|
68
|
+
});
|
|
69
|
+
this.ws.on("message", (data: Buffer) => {
|
|
70
|
+
try {
|
|
71
|
+
const msg = JSON.parse(data.toString());
|
|
72
|
+
if (msg.id != null) {
|
|
73
|
+
const waiter = this.waiters.get(msg.id);
|
|
74
|
+
if (waiter) {
|
|
75
|
+
this.waiters.delete(msg.id);
|
|
76
|
+
waiter.resolve(msg as JsonRpcResponse);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async request(method: string, params?: unknown): Promise<JsonRpcResponse> {
|
|
87
|
+
const id = this.nextId++;
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const timeout = setTimeout(() => {
|
|
90
|
+
this.waiters.delete(id);
|
|
91
|
+
reject(new Error(`MAP request timeout: ${method}`));
|
|
92
|
+
}, 10000);
|
|
93
|
+
this.waiters.set(id, {
|
|
94
|
+
resolve: (r) => { clearTimeout(timeout); resolve(r); },
|
|
95
|
+
reject: (e) => { clearTimeout(timeout); reject(e); },
|
|
96
|
+
});
|
|
97
|
+
this.ws.send(JSON.stringify({ jsonrpc: "2.0", method, params, id }));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
close(): void {
|
|
102
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
103
|
+
this.ws.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface WSMessage {
|
|
109
|
+
type: string;
|
|
110
|
+
[key: string]: unknown;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Simple WebSocket client for API WebSocket at /api/ws.
|
|
115
|
+
* Uses subscribe/unsubscribe protocol (not JSON-RPC).
|
|
116
|
+
*/
|
|
117
|
+
class TestAPIWSClient {
|
|
118
|
+
private ws!: WebSocket;
|
|
119
|
+
private messages: WSMessage[] = [];
|
|
120
|
+
private messageWaiters: Array<{
|
|
121
|
+
check: (msg: WSMessage) => boolean;
|
|
122
|
+
resolve: (msg: WSMessage) => void;
|
|
123
|
+
reject: (err: Error) => void;
|
|
124
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
125
|
+
}> = [];
|
|
126
|
+
private url: string;
|
|
127
|
+
|
|
128
|
+
constructor(url: string) {
|
|
129
|
+
this.url = url;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async connect(): Promise<void> {
|
|
133
|
+
this.ws = new WebSocket(this.url);
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const timeout = setTimeout(() => reject(new Error("API WS connection timeout")), 5000);
|
|
136
|
+
this.ws.on("open", () => {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
resolve();
|
|
139
|
+
});
|
|
140
|
+
this.ws.on("error", (err) => {
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
reject(err);
|
|
143
|
+
});
|
|
144
|
+
this.ws.on("message", (data: Buffer) => {
|
|
145
|
+
try {
|
|
146
|
+
const msg = JSON.parse(data.toString()) as WSMessage;
|
|
147
|
+
this.messages.push(msg);
|
|
148
|
+
|
|
149
|
+
// Check pending waiters
|
|
150
|
+
for (let i = this.messageWaiters.length - 1; i >= 0; i--) {
|
|
151
|
+
const waiter = this.messageWaiters[i];
|
|
152
|
+
if (waiter.check(msg)) {
|
|
153
|
+
clearTimeout(waiter.timeout);
|
|
154
|
+
this.messageWaiters.splice(i, 1);
|
|
155
|
+
waiter.resolve(msg);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// ignore
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async subscribe(channel: string): Promise<void> {
|
|
166
|
+
this.ws.send(JSON.stringify({ type: "subscribe", channel }));
|
|
167
|
+
// Wait for subscribed confirmation
|
|
168
|
+
await this.waitForMessage((msg) => msg.type === "subscribed" && msg.channel === channel);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
waitForMessage(
|
|
172
|
+
check: (msg: WSMessage) => boolean,
|
|
173
|
+
timeoutMs = 5000
|
|
174
|
+
): Promise<WSMessage> {
|
|
175
|
+
// Check existing messages first
|
|
176
|
+
const existing = this.messages.find(check);
|
|
177
|
+
if (existing) return Promise.resolve(existing);
|
|
178
|
+
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const timeout = setTimeout(() => {
|
|
181
|
+
const idx = this.messageWaiters.findIndex((w) => w.resolve === resolve);
|
|
182
|
+
if (idx >= 0) this.messageWaiters.splice(idx, 1);
|
|
183
|
+
reject(new Error("Timeout waiting for WebSocket message"));
|
|
184
|
+
}, timeoutMs);
|
|
185
|
+
|
|
186
|
+
this.messageWaiters.push({ check, resolve, reject, timeout });
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getMessages(): WSMessage[] {
|
|
191
|
+
return [...this.messages];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
clearMessages(): void {
|
|
195
|
+
this.messages.length = 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
close(): void {
|
|
199
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
200
|
+
this.ws.close();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function spawnAgent(eventStore: EventStore, agentId: string, parentId?: string): void {
|
|
206
|
+
const lineage: string[] = [];
|
|
207
|
+
if (parentId) {
|
|
208
|
+
const parent = eventStore.getAgent(parentId as AgentId);
|
|
209
|
+
if (parent) {
|
|
210
|
+
lineage.push(...parent.lineage, parentId);
|
|
211
|
+
} else {
|
|
212
|
+
lineage.push(parentId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
eventStore.emit({
|
|
216
|
+
type: "spawn",
|
|
217
|
+
source: { agent_id: (parentId ?? agentId) as AgentId },
|
|
218
|
+
payload: {
|
|
219
|
+
agent_id: agentId,
|
|
220
|
+
session_id: `session-${agentId}`,
|
|
221
|
+
task: `Task for ${agentId}`,
|
|
222
|
+
parent: parentId ?? null,
|
|
223
|
+
lineage,
|
|
224
|
+
role: "worker",
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─────────────────────────────────────────────────────────────────
|
|
230
|
+
// Tests
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
describe("Mail MAP Protocol E2E", () => {
|
|
234
|
+
let eventStore: EventStore;
|
|
235
|
+
let agentManager: AgentManager;
|
|
236
|
+
let taskManager: TaskManager;
|
|
237
|
+
let messageRouter: MessageRouter;
|
|
238
|
+
let server: CombinedServer;
|
|
239
|
+
let port: number;
|
|
240
|
+
let baseUrl: string;
|
|
241
|
+
const clients: Array<{ close(): void }> = [];
|
|
242
|
+
|
|
243
|
+
beforeEach(async () => {
|
|
244
|
+
port = getRandomPort();
|
|
245
|
+
eventStore = await createEventStore({ inMemory: true });
|
|
246
|
+
messageRouter = createMessageRouter(eventStore);
|
|
247
|
+
taskManager = createTaskManager(eventStore);
|
|
248
|
+
agentManager = createAgentManager(eventStore, messageRouter, {
|
|
249
|
+
defaultPermissionMode: "auto-approve",
|
|
250
|
+
defaultCwd: process.cwd(),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const services: CombinedServerServices = {
|
|
254
|
+
eventStore,
|
|
255
|
+
agentManager,
|
|
256
|
+
taskManager,
|
|
257
|
+
messageRouter,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
server = createCombinedServer(services, { port, host: "localhost" });
|
|
261
|
+
await server.start();
|
|
262
|
+
baseUrl = server.getUrl();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
afterEach(async () => {
|
|
266
|
+
for (const c of clients) {
|
|
267
|
+
c.close();
|
|
268
|
+
}
|
|
269
|
+
clients.length = 0;
|
|
270
|
+
await server.stop().catch(() => {});
|
|
271
|
+
await agentManager.close();
|
|
272
|
+
await eventStore.close();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
function createMAPClient(): TestMAPClient {
|
|
276
|
+
const client = new TestMAPClient(`ws://localhost:${port}/map`);
|
|
277
|
+
clients.push(client);
|
|
278
|
+
return client;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function createAPIWSClient(): TestAPIWSClient {
|
|
282
|
+
const client = new TestAPIWSClient(`ws://localhost:${port}/api/ws`);
|
|
283
|
+
clients.push(client);
|
|
284
|
+
return client;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ─────────────────────────────────────────────────────────────────
|
|
288
|
+
// MAP WebSocket mail/* methods
|
|
289
|
+
// ─────────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
describe("MAP WebSocket mail/* methods", () => {
|
|
292
|
+
describe("mail/create", () => {
|
|
293
|
+
it("creates a conversation via MAP protocol", async () => {
|
|
294
|
+
const client = createMAPClient();
|
|
295
|
+
await client.connect();
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
const res = await client.request("mail/create", {
|
|
299
|
+
type: "session",
|
|
300
|
+
subject: "Test session from MAP",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(res.error).toBeUndefined();
|
|
304
|
+
expect(res.result.conversation).toBeDefined();
|
|
305
|
+
expect(res.result.conversation.id).toMatch(/^conv_/);
|
|
306
|
+
expect(res.result.conversation.type).toBe("session");
|
|
307
|
+
expect(res.result.conversation.status).toBe("active");
|
|
308
|
+
expect(res.result.conversation.subject).toBe("Test session from MAP");
|
|
309
|
+
expect(res.result.conversation.participantCount).toBeGreaterThanOrEqual(1);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("creates conversation with initial participants", async () => {
|
|
313
|
+
const client = createMAPClient();
|
|
314
|
+
await client.connect();
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
const res = await client.request("mail/create", {
|
|
318
|
+
type: "task",
|
|
319
|
+
subject: "Task with participants",
|
|
320
|
+
initialParticipants: [{ id: "agent-1", role: "worker" }],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(res.error).toBeUndefined();
|
|
324
|
+
expect(res.result.conversation.participantCount).toBe(2);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe("mail/list", () => {
|
|
329
|
+
it("lists conversations created by internal agents", async () => {
|
|
330
|
+
// Pre-populate via server's mailService
|
|
331
|
+
const ms = server.mailService!;
|
|
332
|
+
ms.createConversation({ type: "session", subject: "Session 1", createdBy: "user" });
|
|
333
|
+
ms.createConversation({ type: "task", subject: "Task 1", createdBy: "agent-1" });
|
|
334
|
+
ms.createConversation({ type: "task", subject: "Task 2", createdBy: "agent-2" });
|
|
335
|
+
|
|
336
|
+
const client = createMAPClient();
|
|
337
|
+
await client.connect();
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
const res = await client.request("mail/list");
|
|
341
|
+
expect(res.error).toBeUndefined();
|
|
342
|
+
expect(res.result.conversations).toHaveLength(3);
|
|
343
|
+
expect(res.result.hasMore).toBe(false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("filters by type", async () => {
|
|
347
|
+
const ms = server.mailService!;
|
|
348
|
+
ms.createConversation({ type: "session", subject: "S", createdBy: "user" });
|
|
349
|
+
ms.createConversation({ type: "task", subject: "T1", createdBy: "a" });
|
|
350
|
+
ms.createConversation({ type: "task", subject: "T2", createdBy: "b" });
|
|
351
|
+
|
|
352
|
+
const client = createMAPClient();
|
|
353
|
+
await client.connect();
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
const res = await client.request("mail/list", {
|
|
357
|
+
filter: { type: "session" },
|
|
358
|
+
});
|
|
359
|
+
expect(res.result.conversations).toHaveLength(1);
|
|
360
|
+
expect(res.result.conversations[0].type).toBe("session");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("supports cursor pagination with limit", async () => {
|
|
364
|
+
const ms = server.mailService!;
|
|
365
|
+
for (let i = 0; i < 5; i++) {
|
|
366
|
+
ms.createConversation({ type: "task", subject: `Task ${i}`, createdBy: "a" });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const client = createMAPClient();
|
|
370
|
+
await client.connect();
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
const res = await client.request("mail/list", { limit: 2 });
|
|
374
|
+
expect(res.result.conversations).toHaveLength(2);
|
|
375
|
+
expect(res.result.hasMore).toBe(true);
|
|
376
|
+
expect(res.result.nextCursor).toBeDefined();
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("mail/get", () => {
|
|
381
|
+
it("gets conversation details", async () => {
|
|
382
|
+
const ms = server.mailService!;
|
|
383
|
+
const { conversationId } = ms.createConversation({
|
|
384
|
+
type: "session", subject: "Detail test", createdBy: "user",
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const client = createMAPClient();
|
|
388
|
+
await client.connect();
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
const res = await client.request("mail/get", { conversationId });
|
|
392
|
+
expect(res.error).toBeUndefined();
|
|
393
|
+
expect(res.result.conversation.id).toBe(conversationId);
|
|
394
|
+
expect(res.result.conversation.subject).toBe("Detail test");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("includes participants when requested", async () => {
|
|
398
|
+
const ms = server.mailService!;
|
|
399
|
+
const { conversationId } = ms.createConversation({
|
|
400
|
+
type: "task", subject: "P test", createdBy: "a",
|
|
401
|
+
});
|
|
402
|
+
ms.joinConversation({ conversationId, participantId: "a", role: "initiator" });
|
|
403
|
+
ms.joinConversation({ conversationId, participantId: "b", role: "worker" });
|
|
404
|
+
|
|
405
|
+
const client = createMAPClient();
|
|
406
|
+
await client.connect();
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
const res = await client.request("mail/get", {
|
|
410
|
+
conversationId,
|
|
411
|
+
include: { participants: true },
|
|
412
|
+
});
|
|
413
|
+
expect(res.result.participants).toBeDefined();
|
|
414
|
+
expect(res.result.participants).toHaveLength(2);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("includes recentTurns when requested", async () => {
|
|
418
|
+
const ms = server.mailService!;
|
|
419
|
+
const { conversationId } = ms.createConversation({
|
|
420
|
+
type: "session", subject: "Turns test", createdBy: "user",
|
|
421
|
+
});
|
|
422
|
+
for (let i = 0; i < 5; i++) {
|
|
423
|
+
ms.recordTurn({
|
|
424
|
+
conversationId,
|
|
425
|
+
participant: "user",
|
|
426
|
+
contentType: "text",
|
|
427
|
+
content: `Message ${i}`,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const client = createMAPClient();
|
|
432
|
+
await client.connect();
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
const res = await client.request("mail/get", {
|
|
436
|
+
conversationId,
|
|
437
|
+
include: { recentTurns: 3 },
|
|
438
|
+
});
|
|
439
|
+
expect(res.result.recentTurns).toBeDefined();
|
|
440
|
+
expect(res.result.recentTurns).toHaveLength(3);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("includes stats when requested", async () => {
|
|
444
|
+
const ms = server.mailService!;
|
|
445
|
+
const { conversationId } = ms.createConversation({
|
|
446
|
+
type: "session", subject: "Stats test", createdBy: "user",
|
|
447
|
+
});
|
|
448
|
+
ms.joinConversation({ conversationId, participantId: "a" });
|
|
449
|
+
ms.recordTurn({ conversationId, participant: "a", contentType: "text", content: "Hello" });
|
|
450
|
+
|
|
451
|
+
const client = createMAPClient();
|
|
452
|
+
await client.connect();
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
const res = await client.request("mail/get", {
|
|
456
|
+
conversationId,
|
|
457
|
+
include: { stats: true },
|
|
458
|
+
});
|
|
459
|
+
expect(res.result.stats).toBeDefined();
|
|
460
|
+
expect(res.result.stats.totalTurns).toBe(1);
|
|
461
|
+
expect(res.result.stats.activeParticipants).toBeGreaterThanOrEqual(1);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("returns error for non-existent conversation", async () => {
|
|
465
|
+
const client = createMAPClient();
|
|
466
|
+
await client.connect();
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
const res = await client.request("mail/get", { conversationId: "nonexistent" });
|
|
470
|
+
expect(res.error).toBeDefined();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("mail/turn and mail/turns/list", () => {
|
|
475
|
+
it("records a turn from external MAP client", async () => {
|
|
476
|
+
const ms = server.mailService!;
|
|
477
|
+
const { conversationId } = ms.createConversation({
|
|
478
|
+
type: "session", subject: "Turn test", createdBy: "user",
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const client = createMAPClient();
|
|
482
|
+
await client.connect();
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
const res = await client.request("mail/turn", {
|
|
486
|
+
conversationId,
|
|
487
|
+
contentType: "text",
|
|
488
|
+
content: "Hello from MAP client",
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
expect(res.error).toBeUndefined();
|
|
492
|
+
expect(res.result.turn).toBeDefined();
|
|
493
|
+
expect(res.result.turn.content).toBe("Hello from MAP client");
|
|
494
|
+
|
|
495
|
+
// Verify via mailService
|
|
496
|
+
const turns = ms.listTurns({ conversationId });
|
|
497
|
+
expect(turns).toHaveLength(1);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("lists turns from MessageRouter-intercepted messages", async () => {
|
|
501
|
+
const ms = server.mailService!;
|
|
502
|
+
const cm = server.conversationMap!;
|
|
503
|
+
|
|
504
|
+
// Spawn parent + child in EventStore
|
|
505
|
+
spawnAgent(eventStore, "parent-1");
|
|
506
|
+
spawnAgent(eventStore, "child-1", "parent-1");
|
|
507
|
+
|
|
508
|
+
// Set up subscriptions so messages can be delivered
|
|
509
|
+
messageRouter.setupDefaultSubscriptions({
|
|
510
|
+
agent_id: "parent-1" as AgentId,
|
|
511
|
+
});
|
|
512
|
+
messageRouter.setupDefaultSubscriptions({
|
|
513
|
+
agent_id: "child-1" as AgentId,
|
|
514
|
+
parent_id: "parent-1" as AgentId,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Create task conversation and wire ConversationMap
|
|
518
|
+
const { conversationId } = ms.createConversation({
|
|
519
|
+
type: "task", subject: "Task conv", createdBy: "parent-1",
|
|
520
|
+
});
|
|
521
|
+
ms.joinConversation({ conversationId, participantId: "parent-1", role: "initiator" });
|
|
522
|
+
ms.joinConversation({ conversationId, participantId: "child-1", role: "worker" });
|
|
523
|
+
cm.setAgentConversation("child-1", conversationId);
|
|
524
|
+
|
|
525
|
+
// Send messages via router (TurnRecorder intercepts)
|
|
526
|
+
await messageRouter.sendToAddress({
|
|
527
|
+
from: "parent-1" as AgentId,
|
|
528
|
+
to: { agent: "child-1" as AgentId },
|
|
529
|
+
content: "Do this task",
|
|
530
|
+
});
|
|
531
|
+
await messageRouter.sendToAddress({
|
|
532
|
+
from: "child-1" as AgentId,
|
|
533
|
+
to: { agent: "parent-1" as AgentId },
|
|
534
|
+
content: "Done with task",
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Query via MAP protocol
|
|
538
|
+
const client = createMAPClient();
|
|
539
|
+
await client.connect();
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
const res = await client.request("mail/turns/list", { conversationId });
|
|
543
|
+
expect(res.error).toBeUndefined();
|
|
544
|
+
expect(res.result.turns).toHaveLength(2);
|
|
545
|
+
expect(res.result.turns[0].participant).toBe("parent-1");
|
|
546
|
+
expect(res.result.turns[1].participant).toBe("child-1");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("supports pagination", async () => {
|
|
550
|
+
const ms = server.mailService!;
|
|
551
|
+
const { conversationId } = ms.createConversation({
|
|
552
|
+
type: "session", subject: "Paginated", createdBy: "user",
|
|
553
|
+
});
|
|
554
|
+
for (let i = 0; i < 10; i++) {
|
|
555
|
+
ms.recordTurn({ conversationId, participant: "user", contentType: "text", content: `Msg ${i}` });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const client = createMAPClient();
|
|
559
|
+
await client.connect();
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
const res = await client.request("mail/turns/list", { conversationId, limit: 3 });
|
|
563
|
+
expect(res.result.turns).toHaveLength(3);
|
|
564
|
+
expect(res.result.hasMore).toBe(true);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
describe("mail/close", () => {
|
|
569
|
+
it("closes a conversation via MAP protocol", async () => {
|
|
570
|
+
const ms = server.mailService!;
|
|
571
|
+
const { conversationId } = ms.createConversation({
|
|
572
|
+
type: "session", subject: "Close test", createdBy: "user",
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const client = createMAPClient();
|
|
576
|
+
await client.connect();
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
const res = await client.request("mail/close", { conversationId, reason: "completed" });
|
|
580
|
+
expect(res.error).toBeUndefined();
|
|
581
|
+
expect(res.result.conversation.status).toBe("completed");
|
|
582
|
+
|
|
583
|
+
// Verify via mailService
|
|
584
|
+
expect(ms.getConversation(conversationId)!.status).toBe("completed");
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
describe("mail/join and mail/leave", () => {
|
|
589
|
+
it("joins with catch-up history", async () => {
|
|
590
|
+
const ms = server.mailService!;
|
|
591
|
+
const { conversationId } = ms.createConversation({
|
|
592
|
+
type: "task", subject: "Join test", createdBy: "a",
|
|
593
|
+
});
|
|
594
|
+
ms.recordTurn({ conversationId, participant: "a", contentType: "text", content: "Msg 1" });
|
|
595
|
+
ms.recordTurn({ conversationId, participant: "a", contentType: "text", content: "Msg 2" });
|
|
596
|
+
|
|
597
|
+
const client = createMAPClient();
|
|
598
|
+
await client.connect();
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
const res = await client.request("mail/join", {
|
|
602
|
+
conversationId,
|
|
603
|
+
catchUp: { limit: 10 },
|
|
604
|
+
});
|
|
605
|
+
expect(res.error).toBeUndefined();
|
|
606
|
+
expect(res.result.conversation).toBeDefined();
|
|
607
|
+
expect(res.result.history).toBeDefined();
|
|
608
|
+
expect(res.result.history).toHaveLength(2);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("leaves a conversation", async () => {
|
|
612
|
+
const ms = server.mailService!;
|
|
613
|
+
const { conversationId } = ms.createConversation({
|
|
614
|
+
type: "task", subject: "Leave test", createdBy: "a",
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const client = createMAPClient();
|
|
618
|
+
await client.connect();
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
// Join first
|
|
622
|
+
await client.request("mail/join", { conversationId });
|
|
623
|
+
|
|
624
|
+
// Leave
|
|
625
|
+
const res = await client.request("mail/leave", { conversationId });
|
|
626
|
+
expect(res.error).toBeUndefined();
|
|
627
|
+
expect(res.result.success).toBe(true);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
describe("mail/replay", () => {
|
|
632
|
+
it("replays turns in ascending order", async () => {
|
|
633
|
+
const ms = server.mailService!;
|
|
634
|
+
const { conversationId } = ms.createConversation({
|
|
635
|
+
type: "session", subject: "Replay test", createdBy: "user",
|
|
636
|
+
});
|
|
637
|
+
ms.recordTurn({ conversationId, participant: "user", contentType: "text", content: "First" });
|
|
638
|
+
ms.recordTurn({ conversationId, participant: "agent", contentType: "text", content: "Second" });
|
|
639
|
+
ms.recordTurn({ conversationId, participant: "user", contentType: "text", content: "Third" });
|
|
640
|
+
|
|
641
|
+
const client = createMAPClient();
|
|
642
|
+
await client.connect();
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
const res = await client.request("mail/replay", { conversationId });
|
|
646
|
+
expect(res.error).toBeUndefined();
|
|
647
|
+
expect(res.result.turns).toHaveLength(3);
|
|
648
|
+
expect(res.result.turns[0].content).toBe("First");
|
|
649
|
+
expect(res.result.turns[2].content).toBe("Third");
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// ─────────────────────────────────────────────────────────────────
|
|
655
|
+
// REST API /api/conversations
|
|
656
|
+
// ─────────────────────────────────────────────────────────────────
|
|
657
|
+
|
|
658
|
+
describe("REST API /api/conversations", () => {
|
|
659
|
+
it("lists conversations via GET /api/conversations", async () => {
|
|
660
|
+
const ms = server.mailService!;
|
|
661
|
+
ms.createConversation({ type: "session", subject: "S1", createdBy: "user" });
|
|
662
|
+
ms.createConversation({ type: "task", subject: "T1", createdBy: "a" });
|
|
663
|
+
|
|
664
|
+
const res = await fetch(`${baseUrl}/api/conversations`);
|
|
665
|
+
expect(res.status).toBe(200);
|
|
666
|
+
|
|
667
|
+
const body = await res.json();
|
|
668
|
+
expect(body.conversations).toHaveLength(2);
|
|
669
|
+
expect(body.total).toBe(2);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("returns conversation detail via GET /api/conversations/:id", async () => {
|
|
673
|
+
const ms = server.mailService!;
|
|
674
|
+
const { conversationId } = ms.createConversation({
|
|
675
|
+
type: "session", subject: "Detail", createdBy: "user",
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const res = await fetch(`${baseUrl}/api/conversations/${conversationId}`);
|
|
679
|
+
expect(res.status).toBe(200);
|
|
680
|
+
|
|
681
|
+
const body = await res.json();
|
|
682
|
+
expect(body.id).toBe(conversationId);
|
|
683
|
+
expect(body.type).toBe("session");
|
|
684
|
+
expect(body.status).toBe("active");
|
|
685
|
+
expect(body.subject).toBe("Detail");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("returns turns via GET /api/conversations/:id/turns", async () => {
|
|
689
|
+
const ms = server.mailService!;
|
|
690
|
+
const { conversationId } = ms.createConversation({
|
|
691
|
+
type: "session", subject: "Turns", createdBy: "user",
|
|
692
|
+
});
|
|
693
|
+
ms.recordTurn({ conversationId, participant: "user", contentType: "text", content: "Hello" });
|
|
694
|
+
ms.recordTurn({ conversationId, participant: "agent", contentType: "text", content: "Hi" });
|
|
695
|
+
|
|
696
|
+
const res = await fetch(`${baseUrl}/api/conversations/${conversationId}/turns`);
|
|
697
|
+
expect(res.status).toBe(200);
|
|
698
|
+
|
|
699
|
+
const body = await res.json();
|
|
700
|
+
expect(body.turns).toHaveLength(2);
|
|
701
|
+
expect(body.total).toBe(2);
|
|
702
|
+
expect(body.turns[0].participant).toBe("user");
|
|
703
|
+
expect(body.turns[0].content).toBe("Hello");
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("closes via POST /api/conversations/:id/close", async () => {
|
|
707
|
+
const ms = server.mailService!;
|
|
708
|
+
const { conversationId } = ms.createConversation({
|
|
709
|
+
type: "session", subject: "Close", createdBy: "user",
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const res = await fetch(`${baseUrl}/api/conversations/${conversationId}/close`, {
|
|
713
|
+
method: "POST",
|
|
714
|
+
headers: { "Content-Type": "application/json" },
|
|
715
|
+
body: JSON.stringify({ reason: "completed" }),
|
|
716
|
+
});
|
|
717
|
+
expect(res.status).toBe(200);
|
|
718
|
+
|
|
719
|
+
// Verify closed
|
|
720
|
+
expect(ms.getConversation(conversationId)!.status).toBe("completed");
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it("returns participants via GET /api/conversations/:id/participants", async () => {
|
|
724
|
+
const ms = server.mailService!;
|
|
725
|
+
const { conversationId } = ms.createConversation({
|
|
726
|
+
type: "task", subject: "Participants", createdBy: "a",
|
|
727
|
+
});
|
|
728
|
+
ms.joinConversation({ conversationId, participantId: "a", role: "initiator" });
|
|
729
|
+
ms.joinConversation({ conversationId, participantId: "b", role: "worker" });
|
|
730
|
+
|
|
731
|
+
const res = await fetch(`${baseUrl}/api/conversations/${conversationId}/participants`);
|
|
732
|
+
expect(res.status).toBe(200);
|
|
733
|
+
|
|
734
|
+
const body = await res.json();
|
|
735
|
+
expect(body.participants).toHaveLength(2);
|
|
736
|
+
expect(body.total).toBe(2);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("returns 404 for non-existent conversation", async () => {
|
|
740
|
+
const res = await fetch(`${baseUrl}/api/conversations/nonexistent`);
|
|
741
|
+
expect(res.status).toBe(404);
|
|
742
|
+
|
|
743
|
+
const body = await res.json();
|
|
744
|
+
expect(body.code).toBe("CONVERSATION_NOT_FOUND");
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// ─────────────────────────────────────────────────────────────────
|
|
749
|
+
// WebSocket subscription channels
|
|
750
|
+
// ─────────────────────────────────────────────────────────────────
|
|
751
|
+
|
|
752
|
+
describe("WebSocket subscription channels", () => {
|
|
753
|
+
it("receives conversation_update on 'conversations' channel", async () => {
|
|
754
|
+
const ws = createAPIWSClient();
|
|
755
|
+
await ws.connect();
|
|
756
|
+
await ws.subscribe("conversations");
|
|
757
|
+
ws.clearMessages();
|
|
758
|
+
|
|
759
|
+
// Create conversation — triggers onConversationChange
|
|
760
|
+
const ms = server.mailService!;
|
|
761
|
+
ms.createConversation({ type: "session", subject: "WS test", createdBy: "user" });
|
|
762
|
+
|
|
763
|
+
const msg = await ws.waitForMessage((m) => m.type === "conversation_update");
|
|
764
|
+
expect(msg.type).toBe("conversation_update");
|
|
765
|
+
// Strict: wire format is flat { type, conversation }, NOT nested in data
|
|
766
|
+
expect(msg).not.toHaveProperty("data");
|
|
767
|
+
expect((msg as any).conversation).toBeDefined();
|
|
768
|
+
expect((msg as any).conversation.type).toBe("session");
|
|
769
|
+
expect((msg as any).conversation.subject).toBe("WS test");
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("receives turn_added on 'conversation:${id}' channel", async () => {
|
|
773
|
+
const ms = server.mailService!;
|
|
774
|
+
const { conversationId } = ms.createConversation({
|
|
775
|
+
type: "session", subject: "Turn WS", createdBy: "user",
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
const ws = createAPIWSClient();
|
|
779
|
+
await ws.connect();
|
|
780
|
+
await ws.subscribe(`conversation:${conversationId}`);
|
|
781
|
+
ws.clearMessages();
|
|
782
|
+
|
|
783
|
+
// Record turn — triggers onTurnChange
|
|
784
|
+
ms.recordTurn({ conversationId, participant: "user", contentType: "text", content: "WS turn" });
|
|
785
|
+
|
|
786
|
+
const msg = await ws.waitForMessage((m) => m.type === "turn_added");
|
|
787
|
+
expect(msg.type).toBe("turn_added");
|
|
788
|
+
// Strict: wire format is flat { type, conversation_id, turn }
|
|
789
|
+
expect(msg).not.toHaveProperty("data");
|
|
790
|
+
expect((msg as any).conversation_id).toBe(conversationId);
|
|
791
|
+
expect((msg as any).turn).toBeDefined();
|
|
792
|
+
expect((msg as any).turn.content).toBe("WS turn");
|
|
793
|
+
expect((msg as any).turn.participant).toBe("user");
|
|
794
|
+
expect((msg as any).turn.content_type).toBe("text");
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("receives turn_added from MessageRouter-intercepted turns", async () => {
|
|
798
|
+
const ms = server.mailService!;
|
|
799
|
+
const cm = server.conversationMap!;
|
|
800
|
+
|
|
801
|
+
// Spawn parent + child
|
|
802
|
+
spawnAgent(eventStore, "ws-parent");
|
|
803
|
+
spawnAgent(eventStore, "ws-child", "ws-parent");
|
|
804
|
+
messageRouter.setupDefaultSubscriptions({ agent_id: "ws-parent" as AgentId });
|
|
805
|
+
messageRouter.setupDefaultSubscriptions({ agent_id: "ws-child" as AgentId, parent_id: "ws-parent" as AgentId });
|
|
806
|
+
|
|
807
|
+
// Create task conversation
|
|
808
|
+
const { conversationId } = ms.createConversation({
|
|
809
|
+
type: "task", subject: "WS intercept", createdBy: "ws-parent",
|
|
810
|
+
});
|
|
811
|
+
ms.joinConversation({ conversationId, participantId: "ws-parent", role: "initiator" });
|
|
812
|
+
ms.joinConversation({ conversationId, participantId: "ws-child", role: "worker" });
|
|
813
|
+
cm.setAgentConversation("ws-child", conversationId);
|
|
814
|
+
|
|
815
|
+
// Subscribe to conversation channel
|
|
816
|
+
const ws = createAPIWSClient();
|
|
817
|
+
await ws.connect();
|
|
818
|
+
await ws.subscribe(`conversation:${conversationId}`);
|
|
819
|
+
ws.clearMessages();
|
|
820
|
+
|
|
821
|
+
// Send message via router — TurnRecorder intercepts
|
|
822
|
+
await messageRouter.sendToAddress({
|
|
823
|
+
from: "ws-parent" as AgentId,
|
|
824
|
+
to: { agent: "ws-child" as AgentId },
|
|
825
|
+
content: "Intercepted for WS",
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const msg = await ws.waitForMessage((m) => m.type === "turn_added");
|
|
829
|
+
expect(msg.type).toBe("turn_added");
|
|
830
|
+
expect(msg).not.toHaveProperty("data");
|
|
831
|
+
expect((msg as any).turn.participant).toBe("ws-parent");
|
|
832
|
+
expect((msg as any).turn.content).toBe("Intercepted for WS");
|
|
833
|
+
expect((msg as any).turn.source_type).toBe("intercepted");
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it("receives conversation_update when conversation is closed", async () => {
|
|
837
|
+
const ms = server.mailService!;
|
|
838
|
+
const { conversationId } = ms.createConversation({
|
|
839
|
+
type: "session", subject: "Close WS", createdBy: "user",
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const ws = createAPIWSClient();
|
|
843
|
+
await ws.connect();
|
|
844
|
+
await ws.subscribe(`conversation:${conversationId}`);
|
|
845
|
+
ws.clearMessages();
|
|
846
|
+
|
|
847
|
+
ms.closeConversation({ conversationId, closedBy: "user", reason: "completed" });
|
|
848
|
+
|
|
849
|
+
const msg = await ws.waitForMessage((m) => m.type === "conversation_update");
|
|
850
|
+
expect(msg).not.toHaveProperty("data");
|
|
851
|
+
expect((msg as any).conversation.status).toBe("completed");
|
|
852
|
+
expect((msg as any).conversation.id).toBe(conversationId);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("does not receive events for unsubscribed conversations", async () => {
|
|
856
|
+
const ms = server.mailService!;
|
|
857
|
+
const { conversationId: convA } = ms.createConversation({
|
|
858
|
+
type: "session", subject: "Conv A", createdBy: "user",
|
|
859
|
+
});
|
|
860
|
+
const { conversationId: convB } = ms.createConversation({
|
|
861
|
+
type: "session", subject: "Conv B", createdBy: "user",
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
const ws = createAPIWSClient();
|
|
865
|
+
await ws.connect();
|
|
866
|
+
await ws.subscribe(`conversation:${convA}`);
|
|
867
|
+
ws.clearMessages();
|
|
868
|
+
|
|
869
|
+
// Record turn in conv B (not subscribed)
|
|
870
|
+
ms.recordTurn({ conversationId: convB, participant: "user", contentType: "text", content: "B msg" });
|
|
871
|
+
|
|
872
|
+
// Wait briefly — no message should arrive
|
|
873
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
874
|
+
|
|
875
|
+
const turnMessages = ws.getMessages().filter((m) => m.type === "turn_added");
|
|
876
|
+
expect(turnMessages).toHaveLength(0);
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// ─────────────────────────────────────────────────────────────────
|
|
881
|
+
// Validation and error handling
|
|
882
|
+
// ─────────────────────────────────────────────────────────────────
|
|
883
|
+
|
|
884
|
+
describe("Validation and error handling", () => {
|
|
885
|
+
it("mail/close on non-existent conversation returns error", async () => {
|
|
886
|
+
const client = createMAPClient();
|
|
887
|
+
await client.connect();
|
|
888
|
+
|
|
889
|
+
const res = await client.request("mail/close", {
|
|
890
|
+
conversationId: "nonexistent-conv",
|
|
891
|
+
});
|
|
892
|
+
expect(res.error).toBeDefined();
|
|
893
|
+
expect(res.error!.message).toContain("not found");
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it("mail/close on already-closed conversation returns error", async () => {
|
|
897
|
+
const client = createMAPClient();
|
|
898
|
+
await client.connect();
|
|
899
|
+
|
|
900
|
+
const createRes = await client.request("mail/create", {
|
|
901
|
+
type: "session",
|
|
902
|
+
subject: "Close twice test",
|
|
903
|
+
});
|
|
904
|
+
const convId = createRes.result.conversation.id;
|
|
905
|
+
|
|
906
|
+
// Close once — should succeed
|
|
907
|
+
const closeRes1 = await client.request("mail/close", {
|
|
908
|
+
conversationId: convId,
|
|
909
|
+
});
|
|
910
|
+
expect(closeRes1.error).toBeUndefined();
|
|
911
|
+
|
|
912
|
+
// Close again — should fail
|
|
913
|
+
const closeRes2 = await client.request("mail/close", {
|
|
914
|
+
conversationId: convId,
|
|
915
|
+
});
|
|
916
|
+
expect(closeRes2.error).toBeDefined();
|
|
917
|
+
expect(closeRes2.error!.message).toContain("already");
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it("mail/turn on non-existent conversation returns error", async () => {
|
|
921
|
+
const client = createMAPClient();
|
|
922
|
+
await client.connect();
|
|
923
|
+
|
|
924
|
+
const res = await client.request("mail/turn", {
|
|
925
|
+
conversationId: "nonexistent-conv",
|
|
926
|
+
contentType: "text",
|
|
927
|
+
content: "should fail",
|
|
928
|
+
});
|
|
929
|
+
expect(res.error).toBeDefined();
|
|
930
|
+
expect(res.error!.message).toContain("not found");
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it("mail/join on non-existent conversation returns error", async () => {
|
|
934
|
+
const client = createMAPClient();
|
|
935
|
+
await client.connect();
|
|
936
|
+
|
|
937
|
+
const res = await client.request("mail/join", {
|
|
938
|
+
conversationId: "nonexistent-conv",
|
|
939
|
+
});
|
|
940
|
+
expect(res.error).toBeDefined();
|
|
941
|
+
expect(res.error!.message).toContain("not found");
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it("mail/join duplicate participant is idempotent", async () => {
|
|
945
|
+
const client = createMAPClient();
|
|
946
|
+
await client.connect();
|
|
947
|
+
|
|
948
|
+
const createRes = await client.request("mail/create", {
|
|
949
|
+
type: "session",
|
|
950
|
+
subject: "Dup join test",
|
|
951
|
+
});
|
|
952
|
+
const convId = createRes.result.conversation.id;
|
|
953
|
+
|
|
954
|
+
// Creator is auto-joined. Join again — should be idempotent.
|
|
955
|
+
const joinRes = await client.request("mail/join", {
|
|
956
|
+
conversationId: convId,
|
|
957
|
+
role: "worker",
|
|
958
|
+
});
|
|
959
|
+
expect(joinRes.error).toBeUndefined();
|
|
960
|
+
|
|
961
|
+
// Verify no duplicate participants — get the participant list
|
|
962
|
+
const ms = server.mailService!;
|
|
963
|
+
const participants = ms.listParticipants(convId, true);
|
|
964
|
+
|
|
965
|
+
// Group by ID — no participant should appear more than once
|
|
966
|
+
const idCounts = new Map<string, number>();
|
|
967
|
+
for (const p of participants) {
|
|
968
|
+
idCounts.set(p.id, (idCounts.get(p.id) ?? 0) + 1);
|
|
969
|
+
}
|
|
970
|
+
for (const [id, count] of idCounts) {
|
|
971
|
+
expect(count, `Participant ${id} should appear only once`).toBe(1);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it("mail/leave on non-existent conversation returns error", async () => {
|
|
976
|
+
const client = createMAPClient();
|
|
977
|
+
await client.connect();
|
|
978
|
+
|
|
979
|
+
const res = await client.request("mail/leave", {
|
|
980
|
+
conversationId: "nonexistent-conv",
|
|
981
|
+
});
|
|
982
|
+
expect(res.error).toBeDefined();
|
|
983
|
+
expect(res.error!.message).toContain("not found");
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it("mail/thread/create on non-existent conversation returns error", async () => {
|
|
987
|
+
const client = createMAPClient();
|
|
988
|
+
await client.connect();
|
|
989
|
+
|
|
990
|
+
const res = await client.request("mail/thread/create", {
|
|
991
|
+
conversationId: "nonexistent-conv",
|
|
992
|
+
rootTurnId: "turn-1",
|
|
993
|
+
subject: "Thread in void",
|
|
994
|
+
});
|
|
995
|
+
expect(res.error).toBeDefined();
|
|
996
|
+
expect(res.error!.message).toContain("not found");
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it("mail/thread/create returns thread object with nanoid-style ID", async () => {
|
|
1000
|
+
const client = createMAPClient();
|
|
1001
|
+
await client.connect();
|
|
1002
|
+
|
|
1003
|
+
const createRes = await client.request("mail/create", {
|
|
1004
|
+
type: "session",
|
|
1005
|
+
subject: "Thread create test",
|
|
1006
|
+
});
|
|
1007
|
+
const convId = createRes.result.conversation.id;
|
|
1008
|
+
|
|
1009
|
+
// Record a turn to use as root
|
|
1010
|
+
const turnRes = await client.request("mail/turn", {
|
|
1011
|
+
conversationId: convId,
|
|
1012
|
+
contentType: "text",
|
|
1013
|
+
content: "Root turn",
|
|
1014
|
+
});
|
|
1015
|
+
const rootTurnId = turnRes.result.turn.id;
|
|
1016
|
+
|
|
1017
|
+
const threadRes = await client.request("mail/thread/create", {
|
|
1018
|
+
conversationId: convId,
|
|
1019
|
+
rootTurnId,
|
|
1020
|
+
subject: "Discussion thread",
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
expect(threadRes.error).toBeUndefined();
|
|
1024
|
+
expect(threadRes.result.thread).toBeDefined();
|
|
1025
|
+
expect(threadRes.result.thread.id).toMatch(/^thread_[A-Za-z0-9_-]+$/);
|
|
1026
|
+
expect(threadRes.result.thread.subject).toBe("Discussion thread");
|
|
1027
|
+
expect(threadRes.result.thread.conversationId).toBe(convId);
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1032
|
+
// Cross-protocol consistency
|
|
1033
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1034
|
+
|
|
1035
|
+
describe("Cross-protocol consistency", () => {
|
|
1036
|
+
it("MAP-created conversations appear in REST API", async () => {
|
|
1037
|
+
const client = createMAPClient();
|
|
1038
|
+
await client.connect();
|
|
1039
|
+
|
|
1040
|
+
// Create conversation via MAP
|
|
1041
|
+
const createRes = await client.request("mail/create", {
|
|
1042
|
+
type: "task",
|
|
1043
|
+
subject: "Cross-protocol test",
|
|
1044
|
+
});
|
|
1045
|
+
const convId = createRes.result.conversation.id;
|
|
1046
|
+
|
|
1047
|
+
// Record turn via MAP
|
|
1048
|
+
await client.request("mail/turn", {
|
|
1049
|
+
conversationId: convId,
|
|
1050
|
+
contentType: "text",
|
|
1051
|
+
content: "MAP turn content",
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
// Query via REST
|
|
1055
|
+
const restConvRes = await fetch(`${baseUrl}/api/conversations/${convId}`);
|
|
1056
|
+
expect(restConvRes.status).toBe(200);
|
|
1057
|
+
const restConv = await restConvRes.json();
|
|
1058
|
+
expect(restConv.subject).toBe("Cross-protocol test");
|
|
1059
|
+
|
|
1060
|
+
// Query turns via REST
|
|
1061
|
+
const restTurnsRes = await fetch(`${baseUrl}/api/conversations/${convId}/turns`);
|
|
1062
|
+
expect(restTurnsRes.status).toBe(200);
|
|
1063
|
+
const restTurns = await restTurnsRes.json();
|
|
1064
|
+
expect(restTurns.turns).toHaveLength(1);
|
|
1065
|
+
expect(restTurns.turns[0].content).toBe("MAP turn content");
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
});
|