@clinebot/core 0.0.28 → 0.0.30
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/README.md +7 -0
- package/dist/ClineCore.d.ts +28 -2
- package/dist/ClineCore.d.ts.map +1 -1
- package/dist/account/cline-account-service.d.ts +1 -1
- package/dist/account/cline-account-service.d.ts.map +1 -1
- package/dist/account/index.d.ts +1 -1
- package/dist/account/index.d.ts.map +1 -1
- package/dist/account/types.d.ts +5 -0
- package/dist/account/types.d.ts.map +1 -1
- package/dist/auth/bounded-ttl-cache.d.ts +14 -0
- package/dist/auth/bounded-ttl-cache.d.ts.map +1 -0
- package/dist/auth/cline.d.ts +27 -2
- package/dist/auth/cline.d.ts.map +1 -1
- package/dist/auth/oca.d.ts.map +1 -1
- package/dist/chat/chat-schema.d.ts +11 -11
- package/dist/extensions/config/agent-config-loader.d.ts.map +1 -0
- package/dist/{agents → extensions/config}/agent-config-parser.d.ts +2 -2
- package/dist/extensions/config/agent-config-parser.d.ts.map +1 -0
- package/dist/{agents → extensions/config}/hooks-config-loader.d.ts +1 -1
- package/dist/extensions/config/hooks-config-loader.d.ts.map +1 -0
- package/dist/{agents → extensions/config}/index.d.ts +2 -4
- package/dist/extensions/config/index.d.ts.map +1 -0
- package/dist/{runtime/commands.d.ts → extensions/config/runtime-commands.d.ts} +2 -3
- package/dist/extensions/config/runtime-commands.d.ts.map +1 -0
- package/dist/extensions/config/unified-config-file-watcher.d.ts.map +1 -0
- package/dist/extensions/config/user-instruction-config-loader.d.ts.map +1 -0
- package/dist/extensions/context/agentic-compaction.d.ts +13 -0
- package/dist/extensions/context/agentic-compaction.d.ts.map +1 -0
- package/dist/extensions/context/basic-compaction.d.ts +9 -0
- package/dist/extensions/context/basic-compaction.d.ts.map +1 -0
- package/dist/extensions/context/compaction-shared.d.ts +60 -0
- package/dist/extensions/context/compaction-shared.d.ts.map +1 -0
- package/dist/extensions/context/compaction.d.ts +20 -0
- package/dist/extensions/context/compaction.d.ts.map +1 -0
- package/dist/extensions/index.d.ts +5 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/mcp/client.d.ts +3 -0
- package/dist/extensions/mcp/client.d.ts.map +1 -0
- package/dist/extensions/mcp/config-loader.d.ts.map +1 -0
- package/dist/extensions/mcp/index.d.ts +9 -0
- package/dist/extensions/mcp/index.d.ts.map +1 -0
- package/dist/{mcp → extensions/mcp}/manager.d.ts +1 -2
- package/dist/extensions/mcp/manager.d.ts.map +1 -0
- package/dist/extensions/mcp/name-transform.d.ts +3 -0
- package/dist/extensions/mcp/name-transform.d.ts.map +1 -0
- package/dist/extensions/mcp/policies.d.ts +15 -0
- package/dist/extensions/mcp/policies.d.ts.map +1 -0
- package/dist/extensions/mcp/tools.d.ts +4 -0
- package/dist/extensions/mcp/tools.d.ts.map +1 -0
- package/dist/{mcp → extensions/mcp}/types.d.ts +29 -1
- package/dist/extensions/mcp/types.d.ts.map +1 -0
- package/dist/{agents → extensions/plugin}/plugin-config-loader.d.ts +1 -1
- package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -0
- package/dist/{agents → extensions/plugin}/plugin-loader.d.ts +1 -1
- package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -0
- package/dist/extensions/plugin/plugin-module-import.d.ts +5 -0
- package/dist/extensions/plugin/plugin-module-import.d.ts.map +1 -0
- package/dist/{agents → extensions/plugin}/plugin-sandbox.d.ts +1 -1
- package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -0
- package/dist/extensions/plugin-sandbox-bootstrap.js +485 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/persistent.d.ts +64 -0
- package/dist/hooks/persistent.d.ts.map +1 -0
- package/dist/hooks/subprocess-runner.d.ts +22 -0
- package/dist/hooks/subprocess-runner.d.ts.map +1 -0
- package/dist/hooks/subprocess.d.ts +189 -0
- package/dist/hooks/subprocess.d.ts.map +1 -0
- package/dist/index.d.ts +22 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +560 -447
- package/dist/prompt/default-system.d.ts +2 -0
- package/dist/prompt/default-system.d.ts.map +1 -0
- package/dist/providers/local-provider-service.d.ts +1 -1
- package/dist/providers/local-provider-service.d.ts.map +1 -1
- package/dist/runtime/checkpoint-hooks.d.ts +21 -0
- package/dist/runtime/checkpoint-hooks.d.ts.map +1 -0
- package/dist/runtime/hook-file-hooks.d.ts +1 -1
- package/dist/runtime/hook-file-hooks.d.ts.map +1 -1
- package/dist/runtime/rules.d.ts +1 -1
- package/dist/runtime/rules.d.ts.map +1 -1
- package/dist/runtime/runtime-builder.d.ts +1 -1
- package/dist/runtime/runtime-builder.d.ts.map +1 -1
- package/dist/runtime/session-runtime.d.ts +25 -5
- package/dist/runtime/session-runtime.d.ts.map +1 -1
- package/dist/runtime/subprocess-sandbox.d.ts.map +1 -0
- package/dist/runtime/team-runtime-registry.d.ts +1 -1
- package/dist/runtime/team-runtime-registry.d.ts.map +1 -1
- package/dist/runtime/tool-approval.d.ts +1 -1
- package/dist/session/default-session-manager.d.ts +9 -3
- package/dist/session/default-session-manager.d.ts.map +1 -1
- package/dist/session/file-session-service.d.ts +1 -1
- package/dist/session/file-session-service.d.ts.map +1 -1
- package/dist/session/{unified-session-persistence-service.d.ts → persistence-service.d.ts} +11 -42
- package/dist/session/persistence-service.d.ts.map +1 -0
- package/dist/session/rpc-session-service.d.ts +1 -1
- package/dist/session/rpc-session-service.d.ts.map +1 -1
- package/dist/session/session-agent-events.d.ts +1 -1
- package/dist/session/session-artifacts.d.ts.map +1 -1
- package/dist/session/session-config-builder.d.ts.map +1 -1
- package/dist/session/session-graph.d.ts +1 -1
- package/dist/session/session-graph.d.ts.map +1 -1
- package/dist/session/session-host.d.ts.map +1 -1
- package/dist/session/session-manager.d.ts +6 -5
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manifest.d.ts +1 -1
- package/dist/session/session-service.d.ts +3 -2
- package/dist/session/session-service.d.ts.map +1 -1
- package/dist/session/session-team-coordination.d.ts +2 -1
- package/dist/session/session-team-coordination.d.ts.map +1 -1
- package/dist/session/utils/helpers.d.ts +51 -3
- package/dist/session/utils/helpers.d.ts.map +1 -1
- package/dist/session/utils/types.d.ts +41 -7
- package/dist/session/utils/types.d.ts.map +1 -1
- package/dist/session/workspace-manager.d.ts +1 -2
- package/dist/session/workspace-manager.d.ts.map +1 -1
- package/dist/session/workspace-manifest.d.ts +1 -22
- package/dist/session/workspace-manifest.d.ts.map +1 -1
- package/dist/storage/file-team-store.d.ts +2 -1
- package/dist/storage/file-team-store.d.ts.map +1 -1
- package/dist/storage/sqlite-team-store.d.ts +4 -1
- package/dist/storage/sqlite-team-store.d.ts.map +1 -1
- package/dist/storage/team-store.d.ts.map +1 -1
- package/dist/team/delegated-agent.d.ts +44 -0
- package/dist/team/delegated-agent.d.ts.map +1 -0
- package/dist/team/index.d.ts +1 -0
- package/dist/team/index.d.ts.map +1 -1
- package/dist/team/multi-agent.d.ts +229 -0
- package/dist/team/multi-agent.d.ts.map +1 -0
- package/dist/team/projections.d.ts +2 -2
- package/dist/team/projections.d.ts.map +1 -1
- package/dist/team/runtime.d.ts +5 -0
- package/dist/team/runtime.d.ts.map +1 -0
- package/dist/team/spawn-agent-tool.d.ts +85 -0
- package/dist/team/spawn-agent-tool.d.ts.map +1 -0
- package/dist/team/subagent-prompts.d.ts +4 -0
- package/dist/team/subagent-prompts.d.ts.map +1 -0
- package/dist/team/team-tools.d.ts +35 -0
- package/dist/team/team-tools.d.ts.map +1 -0
- package/dist/telemetry/OpenTelemetryProvider.d.ts +11 -1
- package/dist/telemetry/OpenTelemetryProvider.d.ts.map +1 -1
- package/dist/telemetry/{LoggerTelemetryAdapter.d.ts → TelemetryLoggerSink.d.ts} +10 -4
- package/dist/telemetry/TelemetryLoggerSink.d.ts.map +1 -0
- package/dist/telemetry/TelemetryService.d.ts.map +1 -1
- package/dist/telemetry/index.js +15 -28
- package/dist/tools/definitions.d.ts +4 -3
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/index.d.ts +5 -5
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/model-tool-routing.d.ts.map +1 -1
- package/dist/tools/presets.d.ts +26 -0
- package/dist/tools/presets.d.ts.map +1 -1
- package/dist/tools/schemas.d.ts +8 -0
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/types.d.ts +23 -2
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/types/config.d.ts +47 -3
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/events.d.ts +1 -1
- package/dist/types/provider-settings.d.ts +1 -1
- package/dist/types/provider-settings.d.ts.map +1 -1
- package/dist/types/storage.d.ts +2 -1
- package/dist/types/storage.d.ts.map +1 -1
- package/dist/types.d.ts +7 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -12
- package/src/ClineCore.test.ts +150 -0
- package/src/ClineCore.ts +114 -8
- package/src/account/cline-account-service.test.ts +84 -0
- package/src/account/cline-account-service.ts +2 -2
- package/src/account/index.ts +1 -0
- package/src/account/types.ts +6 -0
- package/src/auth/bounded-ttl-cache.test.ts +38 -0
- package/src/auth/bounded-ttl-cache.ts +53 -0
- package/src/auth/cline.test.ts +173 -36
- package/src/auth/cline.ts +395 -93
- package/src/auth/oca.test.ts +125 -0
- package/src/auth/oca.ts +17 -4
- package/src/{agents → extensions/config}/agent-config-loader.test.ts +1 -1
- package/src/{agents → extensions/config}/agent-config-parser.ts +2 -2
- package/src/{agents → extensions/config}/hooks-config-loader.ts +1 -1
- package/src/{agents → extensions/config}/index.ts +7 -11
- package/src/{runtime/commands.test.ts → extensions/config/runtime-commands.test.ts} +20 -3
- package/src/{runtime/commands.ts → extensions/config/runtime-commands.ts} +1 -8
- package/src/{agents → extensions/config}/unified-config-file-watcher.ts +15 -2
- package/src/{agents → extensions/config}/user-instruction-config-loader.test.ts +90 -2
- package/src/{agents → extensions/config}/user-instruction-config-loader.ts +126 -12
- package/src/extensions/context/agentic-compaction.ts +119 -0
- package/src/extensions/context/basic-compaction.ts +275 -0
- package/src/extensions/context/compaction-shared.ts +458 -0
- package/src/extensions/context/compaction.test.ts +477 -0
- package/src/extensions/context/compaction.ts +203 -0
- package/src/extensions/index.ts +12 -0
- package/src/extensions/mcp/client.ts +420 -0
- package/src/{mcp → extensions/mcp}/index.ts +16 -0
- package/src/{mcp → extensions/mcp}/manager.test.ts +1 -2
- package/src/{mcp → extensions/mcp}/manager.ts +3 -5
- package/src/extensions/mcp/name-transform.ts +33 -0
- package/src/extensions/mcp/policies.ts +47 -0
- package/src/extensions/mcp/tools.ts +47 -0
- package/src/{mcp → extensions/mcp}/types.ts +35 -7
- package/src/{agents → extensions/plugin}/plugin-config-loader.test.ts +18 -13
- package/src/{agents → extensions/plugin}/plugin-config-loader.ts +1 -1
- package/src/{agents → extensions/plugin}/plugin-loader.test.ts +41 -4
- package/src/extensions/plugin/plugin-loader.ts +106 -0
- package/src/extensions/plugin/plugin-module-import.ts +278 -0
- package/src/{agents → extensions/plugin}/plugin-sandbox-bootstrap.ts +30 -92
- package/src/{agents → extensions/plugin}/plugin-sandbox.test.ts +60 -3
- package/src/{agents → extensions/plugin}/plugin-sandbox.ts +146 -56
- package/src/hooks/index.ts +25 -0
- package/src/hooks/persistent.ts +661 -0
- package/src/hooks/subprocess-runner.ts +196 -0
- package/src/hooks/subprocess.ts +669 -0
- package/src/index.ts +200 -118
- package/src/prompt/default-system.ts +21 -0
- package/src/providers/local-provider-registry.ts +1 -1
- package/src/providers/local-provider-service.test.ts +23 -2
- package/src/providers/local-provider-service.ts +2 -2
- package/src/runtime/checkpoint-hooks.test.ts +168 -0
- package/src/runtime/checkpoint-hooks.ts +186 -0
- package/src/runtime/hook-file-hooks.test.ts +40 -1
- package/src/runtime/hook-file-hooks.ts +35 -16
- package/src/runtime/index.ts +4 -19
- package/src/runtime/rules.ts +4 -1
- package/src/runtime/runtime-builder.team-persistence.test.ts +3 -6
- package/src/runtime/runtime-builder.test.ts +266 -160
- package/src/runtime/runtime-builder.ts +120 -47
- package/src/runtime/runtime-parity.test.ts +22 -22
- package/src/runtime/session-runtime.ts +36 -6
- package/src/runtime/{sandbox/subprocess-sandbox.ts → subprocess-sandbox.ts} +24 -3
- package/src/runtime/team-runtime-registry.ts +1 -4
- package/src/runtime/tool-approval.ts +1 -1
- package/src/session/default-session-manager.e2e.test.ts +2 -2
- package/src/session/default-session-manager.test.ts +553 -9
- package/src/session/default-session-manager.ts +162 -46
- package/src/session/file-session-service.ts +3 -3
- package/src/session/index.ts +6 -6
- package/src/session/persistence-service.test.ts +212 -0
- package/src/session/{unified-session-persistence-service.ts → persistence-service.ts} +106 -172
- package/src/session/rpc-session-service.ts +3 -3
- package/src/session/runtime-oauth-token-manager.ts +1 -1
- package/src/session/session-agent-events.ts +1 -1
- package/src/session/session-artifacts.ts +32 -4
- package/src/session/session-config-builder.ts +22 -9
- package/src/session/session-graph.ts +1 -1
- package/src/session/session-host.ts +19 -11
- package/src/session/session-manager.ts +11 -6
- package/src/session/session-service.team-persistence.test.ts +1 -1
- package/src/session/session-service.ts +6 -9
- package/src/session/session-team-coordination.ts +7 -3
- package/src/session/session-telemetry.ts +1 -1
- package/src/session/utils/helpers.test.ts +160 -0
- package/src/session/utils/helpers.ts +289 -42
- package/src/session/utils/types.ts +47 -7
- package/src/session/workspace-manager.ts +5 -3
- package/src/session/workspace-manifest.ts +3 -49
- package/src/storage/file-team-store.ts +2 -5
- package/src/storage/provider-settings-legacy-migration.ts +2 -2
- package/src/storage/provider-settings-manager.test.ts +1 -1
- package/src/storage/sqlite-team-store.ts +212 -125
- package/src/storage/team-store.ts +1 -5
- package/src/team/delegated-agent.ts +131 -0
- package/src/team/index.ts +1 -0
- package/src/team/multi-agent.lifecycle.test.ts +201 -0
- package/src/team/multi-agent.ts +1666 -0
- package/src/team/projections.ts +2 -4
- package/src/team/runtime.ts +54 -0
- package/src/team/spawn-agent-tool.test.ts +387 -0
- package/src/team/spawn-agent-tool.ts +207 -0
- package/src/team/subagent-prompts.ts +41 -0
- package/src/team/team-tools.test.ts +802 -0
- package/src/team/team-tools.ts +792 -0
- package/src/telemetry/OpenTelemetryAdapter.ts +1 -1
- package/src/telemetry/OpenTelemetryProvider.test.ts +216 -3
- package/src/telemetry/OpenTelemetryProvider.ts +110 -20
- package/src/telemetry/TelemetryLoggerSink.test.ts +42 -0
- package/src/telemetry/{LoggerTelemetryAdapter.ts → TelemetryLoggerSink.ts} +21 -14
- package/src/telemetry/TelemetryService.test.ts +7 -7
- package/src/telemetry/TelemetryService.ts +2 -4
- package/src/tools/definitions.test.ts +76 -0
- package/src/tools/definitions.ts +41 -2
- package/src/tools/executors/apply-patch.ts +1 -1
- package/src/tools/executors/editor.ts +1 -1
- package/src/tools/executors/file-read.ts +1 -1
- package/src/tools/executors/search.ts +1 -1
- package/src/tools/executors/web-fetch.ts +1 -1
- package/src/tools/index.ts +6 -1
- package/src/tools/model-tool-routing.ts +2 -0
- package/src/tools/presets.test.ts +8 -0
- package/src/tools/presets.ts +40 -2
- package/src/tools/schemas.ts +19 -0
- package/src/tools/types.ts +31 -2
- package/src/types/config.ts +61 -7
- package/src/types/events.ts +1 -1
- package/src/types/index.ts +0 -1
- package/src/types/provider-settings.ts +1 -1
- package/src/types/storage.ts +2 -5
- package/src/types.ts +32 -44
- package/dist/agents/agent-config-loader.d.ts.map +0 -1
- package/dist/agents/agent-config-parser.d.ts.map +0 -1
- package/dist/agents/hooks-config-loader.d.ts.map +0 -1
- package/dist/agents/index.d.ts.map +0 -1
- package/dist/agents/plugin-config-loader.d.ts.map +0 -1
- package/dist/agents/plugin-loader.d.ts.map +0 -1
- package/dist/agents/plugin-sandbox-bootstrap.js +0 -446
- package/dist/agents/plugin-sandbox.d.ts.map +0 -1
- package/dist/agents/unified-config-file-watcher.d.ts.map +0 -1
- package/dist/agents/user-instruction-config-loader.d.ts.map +0 -1
- package/dist/mcp/config-loader.d.ts.map +0 -1
- package/dist/mcp/index.d.ts +0 -5
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/manager.d.ts.map +0 -1
- package/dist/mcp/types.d.ts.map +0 -1
- package/dist/runtime/commands.d.ts.map +0 -1
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts.map +0 -1
- package/dist/runtime/skills.d.ts +0 -14
- package/dist/runtime/skills.d.ts.map +0 -1
- package/dist/runtime/workflows.d.ts +0 -14
- package/dist/runtime/workflows.d.ts.map +0 -1
- package/dist/session/unified-session-persistence-service.d.ts.map +0 -1
- package/dist/telemetry/LoggerTelemetryAdapter.d.ts.map +0 -1
- package/dist/types/workspace.d.ts +0 -8
- package/dist/types/workspace.d.ts.map +0 -1
- package/src/agents/plugin-loader.ts +0 -175
- package/src/runtime/skills.ts +0 -44
- package/src/runtime/workflows.test.ts +0 -119
- package/src/runtime/workflows.ts +0 -45
- package/src/session/unified-session-persistence-service.test.ts +0 -85
- package/src/telemetry/LoggerTelemetryAdapter.test.ts +0 -42
- package/src/types/workspace.ts +0 -7
- /package/dist/{agents → extensions/config}/agent-config-loader.d.ts +0 -0
- /package/dist/{agents → extensions/config}/unified-config-file-watcher.d.ts +0 -0
- /package/dist/{agents → extensions/config}/user-instruction-config-loader.d.ts +0 -0
- /package/dist/{mcp → extensions/mcp}/config-loader.d.ts +0 -0
- /package/dist/runtime/{sandbox/subprocess-sandbox.d.ts → subprocess-sandbox.d.ts} +0 -0
- /package/src/{agents → extensions/config}/agent-config-loader.ts +0 -0
- /package/src/{agents → extensions/config}/hooks-config-loader.test.ts +0 -0
- /package/src/{agents → extensions/config}/unified-config-file-watcher.test.ts +0 -0
- /package/src/{mcp → extensions/mcp}/config-loader.test.ts +0 -0
- /package/src/{mcp → extensions/mcp}/config-loader.ts +0 -0
|
@@ -0,0 +1,1666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Agent Coordination
|
|
3
|
+
*
|
|
4
|
+
* Utilities for orchestrating multiple agents working together.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type Agent, createAgent } from "@clinebot/agents";
|
|
8
|
+
import {
|
|
9
|
+
type AgentConfig,
|
|
10
|
+
type AgentEvent,
|
|
11
|
+
type AgentResult,
|
|
12
|
+
type AppendMissionLogInput,
|
|
13
|
+
type AttachTeamOutcomeFragmentInput,
|
|
14
|
+
type CreateTeamOutcomeInput,
|
|
15
|
+
type CreateTeamTaskInput,
|
|
16
|
+
type MissionLogEntry,
|
|
17
|
+
type ReviewTeamOutcomeFragmentInput,
|
|
18
|
+
type RouteToTeammateOptions,
|
|
19
|
+
sanitizeFileName,
|
|
20
|
+
type TeamMailboxMessage,
|
|
21
|
+
type TeamMemberSnapshot,
|
|
22
|
+
TeamMessageType,
|
|
23
|
+
type TeammateLifecycleSpec,
|
|
24
|
+
type TeamOutcome,
|
|
25
|
+
type TeamOutcomeFragment,
|
|
26
|
+
type TeamOutcomeStatus,
|
|
27
|
+
type TeamRunRecord,
|
|
28
|
+
type TeamRunStatus,
|
|
29
|
+
type TeamRuntimeSnapshot,
|
|
30
|
+
type TeamRuntimeState,
|
|
31
|
+
type TeamTask,
|
|
32
|
+
type TeamTaskListItem,
|
|
33
|
+
type TeamTaskStatus,
|
|
34
|
+
} from "@clinebot/shared";
|
|
35
|
+
import { nanoid } from "nanoid";
|
|
36
|
+
|
|
37
|
+
// Re-export shared types for backward compatibility
|
|
38
|
+
export {
|
|
39
|
+
type AppendMissionLogInput,
|
|
40
|
+
type AttachTeamOutcomeFragmentInput,
|
|
41
|
+
type CreateTeamOutcomeInput,
|
|
42
|
+
type CreateTeamTaskInput,
|
|
43
|
+
type MissionLogEntry,
|
|
44
|
+
type MissionLogKind,
|
|
45
|
+
type ReviewTeamOutcomeFragmentInput,
|
|
46
|
+
type RouteToTeammateOptions,
|
|
47
|
+
type TeamMailboxMessage,
|
|
48
|
+
type TeamMemberSnapshot,
|
|
49
|
+
TeamMessageType,
|
|
50
|
+
type TeammateLifecycleSpec,
|
|
51
|
+
type TeamOutcome,
|
|
52
|
+
type TeamOutcomeFragment,
|
|
53
|
+
type TeamOutcomeFragmentStatus,
|
|
54
|
+
type TeamOutcomeStatus,
|
|
55
|
+
type TeamRunRecord,
|
|
56
|
+
type TeamRunStatus,
|
|
57
|
+
type TeamRuntimeSnapshot,
|
|
58
|
+
type TeamRuntimeState,
|
|
59
|
+
type TeamTask,
|
|
60
|
+
type TeamTaskListItem,
|
|
61
|
+
type TeamTaskStatus,
|
|
62
|
+
} from "@clinebot/shared";
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// Types that depend on @clinebot/agents (cannot live in shared)
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
export interface TeamMemberConfig extends AgentConfig {
|
|
69
|
+
role?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AgentTask {
|
|
73
|
+
agentId: string;
|
|
74
|
+
message: string;
|
|
75
|
+
metadata?: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TaskResult {
|
|
79
|
+
agentId: string;
|
|
80
|
+
result: AgentResult;
|
|
81
|
+
error?: Error;
|
|
82
|
+
metadata?: Record<string, unknown>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type TeamEvent =
|
|
86
|
+
| { type: TeamMessageType.TaskStart; agentId: string; message: string }
|
|
87
|
+
| {
|
|
88
|
+
type: TeamMessageType.TaskEnd;
|
|
89
|
+
agentId: string;
|
|
90
|
+
result?: AgentResult;
|
|
91
|
+
error?: Error;
|
|
92
|
+
messages?: AgentResult["messages"];
|
|
93
|
+
}
|
|
94
|
+
| { type: TeamMessageType.AgentEvent; agentId: string; event: AgentEvent }
|
|
95
|
+
| {
|
|
96
|
+
type: TeamMessageType.TeammateSpawned;
|
|
97
|
+
agentId: string;
|
|
98
|
+
role?: string;
|
|
99
|
+
teammate: TeammateLifecycleSpec;
|
|
100
|
+
}
|
|
101
|
+
| { type: TeamMessageType.TeammateShutdown; agentId: string; reason?: string }
|
|
102
|
+
| { type: TeamMessageType.TeamTaskUpdated; task: TeamTask }
|
|
103
|
+
| { type: TeamMessageType.TeamMessage; message: TeamMailboxMessage }
|
|
104
|
+
| { type: TeamMessageType.TeamMissionLog; entry: MissionLogEntry }
|
|
105
|
+
| { type: TeamMessageType.RunQueued; run: TeamRunRecord }
|
|
106
|
+
| { type: TeamMessageType.RunStarted; run: TeamRunRecord }
|
|
107
|
+
| { type: TeamMessageType.RunProgress; run: TeamRunRecord; message: string }
|
|
108
|
+
| { type: TeamMessageType.RunCompleted; run: TeamRunRecord }
|
|
109
|
+
| { type: TeamMessageType.RunFailed; run: TeamRunRecord }
|
|
110
|
+
| { type: TeamMessageType.RunCancelled; run: TeamRunRecord; reason?: string }
|
|
111
|
+
| {
|
|
112
|
+
type: TeamMessageType.RunInterrupted;
|
|
113
|
+
run: TeamRunRecord;
|
|
114
|
+
reason?: string;
|
|
115
|
+
}
|
|
116
|
+
| { type: TeamMessageType.OutcomeCreated; outcome: TeamOutcome }
|
|
117
|
+
| {
|
|
118
|
+
type: TeamMessageType.OutcomeFragmentAttached;
|
|
119
|
+
fragment: TeamOutcomeFragment;
|
|
120
|
+
}
|
|
121
|
+
| {
|
|
122
|
+
type: TeamMessageType.OutcomeFragmentReviewed;
|
|
123
|
+
fragment: TeamOutcomeFragment;
|
|
124
|
+
}
|
|
125
|
+
| { type: TeamMessageType.OutcomeFinalized; outcome: TeamOutcome };
|
|
126
|
+
|
|
127
|
+
export interface AgentTeamsRuntimeOptions {
|
|
128
|
+
teamName: string;
|
|
129
|
+
leadAgentId?: string;
|
|
130
|
+
missionLogIntervalSteps?: number;
|
|
131
|
+
missionLogIntervalMs?: number;
|
|
132
|
+
maxConcurrentRuns?: number;
|
|
133
|
+
onTeamEvent?: (event: TeamEvent) => void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface SpawnTeammateOptions {
|
|
137
|
+
agentId: string;
|
|
138
|
+
config: TeamMemberConfig;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// =============================================================================
|
|
142
|
+
// AgentTeam
|
|
143
|
+
// =============================================================================
|
|
144
|
+
|
|
145
|
+
const TEAMMATE_API_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
146
|
+
|
|
147
|
+
export class AgentTeam {
|
|
148
|
+
private agents: Map<string, Agent> = new Map();
|
|
149
|
+
private configs: Map<string, TeamMemberConfig> = new Map();
|
|
150
|
+
private onTeamEvent?: (event: TeamEvent) => void;
|
|
151
|
+
|
|
152
|
+
constructor(
|
|
153
|
+
configs?: Record<string, TeamMemberConfig>,
|
|
154
|
+
onTeamEvent?: (event: TeamEvent) => void,
|
|
155
|
+
) {
|
|
156
|
+
this.onTeamEvent = onTeamEvent;
|
|
157
|
+
|
|
158
|
+
if (configs) {
|
|
159
|
+
for (const [id, config] of Object.entries(configs)) {
|
|
160
|
+
this.addAgent(id, config);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
addAgent(id: string, config: TeamMemberConfig): void {
|
|
166
|
+
if (this.agents.has(id)) {
|
|
167
|
+
throw new Error(`Agent with id "${id}" already exists in the team`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const wrappedConfig: AgentConfig = {
|
|
171
|
+
...config,
|
|
172
|
+
onEvent: (event: AgentEvent) => {
|
|
173
|
+
config.onEvent?.(event);
|
|
174
|
+
this.emitEvent({
|
|
175
|
+
type: TeamMessageType.AgentEvent,
|
|
176
|
+
agentId: id,
|
|
177
|
+
event,
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const agent = createAgent(wrappedConfig);
|
|
183
|
+
this.agents.set(id, agent);
|
|
184
|
+
this.configs.set(id, config);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
removeAgent(id: string): boolean {
|
|
188
|
+
this.configs.delete(id);
|
|
189
|
+
return this.agents.delete(id);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getAgent(id: string): Agent | undefined {
|
|
193
|
+
return this.agents.get(id);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
getAgentIds(): string[] {
|
|
197
|
+
return Array.from(this.agents.keys());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get size(): number {
|
|
201
|
+
return this.agents.size;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async routeTo(agentId: string, message: string): Promise<AgentResult> {
|
|
205
|
+
const agent = this.agents.get(agentId);
|
|
206
|
+
if (!agent) {
|
|
207
|
+
throw new Error(`Agent "${agentId}" not found in team`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.emitEvent({ type: TeamMessageType.TaskStart, agentId, message });
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const result = await agent.run(message);
|
|
214
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, result });
|
|
215
|
+
return result;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
218
|
+
this.emitEvent({
|
|
219
|
+
type: TeamMessageType.TaskEnd,
|
|
220
|
+
agentId,
|
|
221
|
+
error: err,
|
|
222
|
+
messages: agent.getMessages(),
|
|
223
|
+
});
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async continueTo(agentId: string, message: string): Promise<AgentResult> {
|
|
229
|
+
const agent = this.agents.get(agentId);
|
|
230
|
+
if (!agent) {
|
|
231
|
+
throw new Error(`Agent "${agentId}" not found in team`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.emitEvent({ type: TeamMessageType.TaskStart, agentId, message });
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const result = await agent.continue(message);
|
|
238
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, result });
|
|
239
|
+
return result;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
242
|
+
this.emitEvent({
|
|
243
|
+
type: TeamMessageType.TaskEnd,
|
|
244
|
+
agentId,
|
|
245
|
+
error: err,
|
|
246
|
+
messages: agent.getMessages(),
|
|
247
|
+
});
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async runParallel(tasks: AgentTask[]): Promise<TaskResult[]> {
|
|
253
|
+
const executions = tasks.map(async (task): Promise<TaskResult> => {
|
|
254
|
+
const agent = this.agents.get(task.agentId);
|
|
255
|
+
if (!agent) {
|
|
256
|
+
return {
|
|
257
|
+
agentId: task.agentId,
|
|
258
|
+
result: undefined as unknown as AgentResult,
|
|
259
|
+
error: new Error(`Agent "${task.agentId}" not found in team`),
|
|
260
|
+
metadata: task.metadata,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.emitEvent({
|
|
265
|
+
type: TeamMessageType.TaskStart,
|
|
266
|
+
agentId: task.agentId,
|
|
267
|
+
message: task.message,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const result = await agent.run(task.message);
|
|
272
|
+
this.emitEvent({
|
|
273
|
+
type: TeamMessageType.TaskEnd,
|
|
274
|
+
agentId: task.agentId,
|
|
275
|
+
result,
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
agentId: task.agentId,
|
|
279
|
+
result,
|
|
280
|
+
metadata: task.metadata,
|
|
281
|
+
};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
284
|
+
this.emitEvent({
|
|
285
|
+
type: TeamMessageType.TaskEnd,
|
|
286
|
+
agentId: task.agentId,
|
|
287
|
+
error: err,
|
|
288
|
+
messages: agent.getMessages(),
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
agentId: task.agentId,
|
|
292
|
+
result: undefined as unknown as AgentResult,
|
|
293
|
+
error: err,
|
|
294
|
+
metadata: task.metadata,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return Promise.all(executions);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async runSequential(tasks: AgentTask[]): Promise<TaskResult[]> {
|
|
303
|
+
const results: TaskResult[] = [];
|
|
304
|
+
|
|
305
|
+
for (const task of tasks) {
|
|
306
|
+
const agent = this.agents.get(task.agentId);
|
|
307
|
+
if (!agent) {
|
|
308
|
+
results.push({
|
|
309
|
+
agentId: task.agentId,
|
|
310
|
+
result: undefined as unknown as AgentResult,
|
|
311
|
+
error: new Error(`Agent "${task.agentId}" not found in team`),
|
|
312
|
+
metadata: task.metadata,
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.emitEvent({
|
|
318
|
+
type: TeamMessageType.TaskStart,
|
|
319
|
+
agentId: task.agentId,
|
|
320
|
+
message: task.message,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const result = await agent.run(task.message);
|
|
325
|
+
this.emitEvent({
|
|
326
|
+
type: TeamMessageType.TaskEnd,
|
|
327
|
+
agentId: task.agentId,
|
|
328
|
+
result,
|
|
329
|
+
});
|
|
330
|
+
results.push({
|
|
331
|
+
agentId: task.agentId,
|
|
332
|
+
result,
|
|
333
|
+
metadata: task.metadata,
|
|
334
|
+
});
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
337
|
+
this.emitEvent({
|
|
338
|
+
type: TeamMessageType.TaskEnd,
|
|
339
|
+
agentId: task.agentId,
|
|
340
|
+
error: err,
|
|
341
|
+
messages: agent.getMessages(),
|
|
342
|
+
});
|
|
343
|
+
results.push({
|
|
344
|
+
agentId: task.agentId,
|
|
345
|
+
result: undefined as unknown as AgentResult,
|
|
346
|
+
error: err,
|
|
347
|
+
metadata: task.metadata,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return results;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async runPipeline(
|
|
356
|
+
pipeline: string[],
|
|
357
|
+
initialMessage: string,
|
|
358
|
+
messageTransformer?: (
|
|
359
|
+
prevResult: AgentResult,
|
|
360
|
+
nextAgentId: string,
|
|
361
|
+
) => string,
|
|
362
|
+
): Promise<TaskResult[]> {
|
|
363
|
+
const results: TaskResult[] = [];
|
|
364
|
+
let currentMessage = initialMessage;
|
|
365
|
+
|
|
366
|
+
for (const agentId of pipeline) {
|
|
367
|
+
const agent = this.agents.get(agentId);
|
|
368
|
+
if (!agent) {
|
|
369
|
+
results.push({
|
|
370
|
+
agentId,
|
|
371
|
+
result: undefined as unknown as AgentResult,
|
|
372
|
+
error: new Error(`Agent "${agentId}" not found in team`),
|
|
373
|
+
});
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.emitEvent({
|
|
378
|
+
type: TeamMessageType.TaskStart,
|
|
379
|
+
agentId,
|
|
380
|
+
message: currentMessage,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const result = await agent.run(currentMessage);
|
|
385
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, result });
|
|
386
|
+
results.push({ agentId, result });
|
|
387
|
+
|
|
388
|
+
const nextIndex = pipeline.indexOf(agentId) + 1;
|
|
389
|
+
if (nextIndex < pipeline.length) {
|
|
390
|
+
const nextAgentId = pipeline[nextIndex];
|
|
391
|
+
currentMessage = messageTransformer
|
|
392
|
+
? messageTransformer(result, nextAgentId)
|
|
393
|
+
: `Previous agent output:\n${result.text}\n\nPlease continue from here.`;
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
397
|
+
this.emitEvent({
|
|
398
|
+
type: TeamMessageType.TaskEnd,
|
|
399
|
+
agentId,
|
|
400
|
+
error: err,
|
|
401
|
+
messages: agent.getMessages(),
|
|
402
|
+
});
|
|
403
|
+
results.push({
|
|
404
|
+
agentId,
|
|
405
|
+
result: undefined as unknown as AgentResult,
|
|
406
|
+
error: err,
|
|
407
|
+
});
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return results;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
abortAll(): void {
|
|
416
|
+
for (const agent of this.agents.values()) {
|
|
417
|
+
agent.abort(new Error("Agent team abortAll requested"));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
clear(): void {
|
|
422
|
+
this.abortAll();
|
|
423
|
+
this.agents.clear();
|
|
424
|
+
this.configs.clear();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private emitEvent(event: TeamEvent): void {
|
|
428
|
+
try {
|
|
429
|
+
this.onTeamEvent?.(event);
|
|
430
|
+
} catch {
|
|
431
|
+
// Ignore callback errors
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// =============================================================================
|
|
437
|
+
// Factory Functions
|
|
438
|
+
// =============================================================================
|
|
439
|
+
|
|
440
|
+
export function createAgentTeam(
|
|
441
|
+
configs: Record<string, TeamMemberConfig>,
|
|
442
|
+
onTeamEvent?: (event: TeamEvent) => void,
|
|
443
|
+
): AgentTeam {
|
|
444
|
+
return new AgentTeam(configs, onTeamEvent);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function createWorkerReviewerTeam(configs: {
|
|
448
|
+
worker: TeamMemberConfig;
|
|
449
|
+
reviewer: TeamMemberConfig;
|
|
450
|
+
}): AgentTeam & {
|
|
451
|
+
doAndReview: (
|
|
452
|
+
message: string,
|
|
453
|
+
) => Promise<{ workerResult: AgentResult; reviewResult: AgentResult }>;
|
|
454
|
+
} {
|
|
455
|
+
const team = createAgentTeam({
|
|
456
|
+
worker: configs.worker,
|
|
457
|
+
reviewer: configs.reviewer,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const enhanced = team as AgentTeam & {
|
|
461
|
+
doAndReview: (
|
|
462
|
+
message: string,
|
|
463
|
+
) => Promise<{ workerResult: AgentResult; reviewResult: AgentResult }>;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
enhanced.doAndReview = async (message: string) => {
|
|
467
|
+
const workerResult = await team.routeTo("worker", message);
|
|
468
|
+
const reviewResult = await team.routeTo(
|
|
469
|
+
"reviewer",
|
|
470
|
+
`Please review this work:\n\n${workerResult.text}`,
|
|
471
|
+
);
|
|
472
|
+
return { workerResult, reviewResult };
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
return enhanced;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// =============================================================================
|
|
479
|
+
// Agent Teams Runtime (lead + teammate collaboration)
|
|
480
|
+
// =============================================================================
|
|
481
|
+
|
|
482
|
+
interface TeamMemberState extends TeamMemberSnapshot {
|
|
483
|
+
agent?: Agent;
|
|
484
|
+
runningCount: number;
|
|
485
|
+
lastMissionStep: number;
|
|
486
|
+
lastMissionAt: number;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export class AgentTeamsRuntime {
|
|
490
|
+
private readonly teamId: string;
|
|
491
|
+
private readonly teamName: string;
|
|
492
|
+
private readonly onTeamEvent?: (event: TeamEvent) => void;
|
|
493
|
+
private readonly members: Map<string, TeamMemberState> = new Map();
|
|
494
|
+
private readonly tasks: Map<string, TeamTask> = new Map();
|
|
495
|
+
private readonly missionLog: MissionLogEntry[] = [];
|
|
496
|
+
private readonly mailbox: TeamMailboxMessage[] = [];
|
|
497
|
+
private missionStepCounter = 0;
|
|
498
|
+
private taskCounter = 0;
|
|
499
|
+
private messageCounter = 0;
|
|
500
|
+
private missionCounter = 0;
|
|
501
|
+
private runCounter = 0;
|
|
502
|
+
private outcomeCounter = 0;
|
|
503
|
+
private outcomeFragmentCounter = 0;
|
|
504
|
+
private readonly runs: Map<string, TeamRunRecord & { result?: AgentResult }> =
|
|
505
|
+
new Map();
|
|
506
|
+
private readonly runQueue: string[] = [];
|
|
507
|
+
private readonly outcomes: Map<string, TeamOutcome> = new Map();
|
|
508
|
+
private readonly outcomeFragments: Map<string, TeamOutcomeFragment> =
|
|
509
|
+
new Map();
|
|
510
|
+
private readonly missionLogIntervalSteps: number;
|
|
511
|
+
private readonly missionLogIntervalMs: number;
|
|
512
|
+
private readonly maxConcurrentRuns: number;
|
|
513
|
+
|
|
514
|
+
constructor(options: AgentTeamsRuntimeOptions) {
|
|
515
|
+
this.teamName = options.teamName;
|
|
516
|
+
this.teamId = `t_${sanitizeFileName(nanoid(10))}`;
|
|
517
|
+
this.onTeamEvent = options.onTeamEvent;
|
|
518
|
+
this.missionLogIntervalSteps = Math.max(
|
|
519
|
+
1,
|
|
520
|
+
options.missionLogIntervalSteps ?? 3,
|
|
521
|
+
);
|
|
522
|
+
this.missionLogIntervalMs = Math.max(
|
|
523
|
+
1000,
|
|
524
|
+
options.missionLogIntervalMs ?? 120000,
|
|
525
|
+
);
|
|
526
|
+
this.maxConcurrentRuns = Math.max(1, options.maxConcurrentRuns ?? 2);
|
|
527
|
+
const leadAgentId = options.leadAgentId ?? "lead";
|
|
528
|
+
this.members.set(leadAgentId, {
|
|
529
|
+
agentId: leadAgentId,
|
|
530
|
+
role: "lead",
|
|
531
|
+
status: "idle",
|
|
532
|
+
runningCount: 0,
|
|
533
|
+
lastMissionStep: 0,
|
|
534
|
+
lastMissionAt: Date.now(),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
getTeamId(): string {
|
|
539
|
+
return this.teamId;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
getTeamName(): string {
|
|
543
|
+
return this.teamName;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
getMemberRole(agentId: string): "lead" | "teammate" | undefined {
|
|
547
|
+
return this.members.get(agentId)?.role;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
getMemberIds(): string[] {
|
|
551
|
+
return Array.from(this.members.keys());
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
getTeammateIds(): string[] {
|
|
555
|
+
return Array.from(this.members.values())
|
|
556
|
+
.filter((member) => member.role === "teammate")
|
|
557
|
+
.map((member) => member.agentId);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
getTask(taskId: string): TeamTask | undefined {
|
|
561
|
+
return this.tasks.get(taskId);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
listTasks(): TeamTask[] {
|
|
565
|
+
return Array.from(this.tasks.values());
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
listTaskItems(options?: {
|
|
569
|
+
status?: TeamTaskStatus;
|
|
570
|
+
assignee?: string;
|
|
571
|
+
unassignedOnly?: boolean;
|
|
572
|
+
readyOnly?: boolean;
|
|
573
|
+
}): TeamTaskListItem[] {
|
|
574
|
+
return Array.from(this.tasks.values())
|
|
575
|
+
.map((task) => {
|
|
576
|
+
const blockedBy = this.getUnresolvedDependencies(task);
|
|
577
|
+
return {
|
|
578
|
+
...task,
|
|
579
|
+
blockedBy,
|
|
580
|
+
isReady:
|
|
581
|
+
task.status === "pending" &&
|
|
582
|
+
!task.assignee &&
|
|
583
|
+
blockedBy.length === 0,
|
|
584
|
+
};
|
|
585
|
+
})
|
|
586
|
+
.filter((task) => {
|
|
587
|
+
if (options?.status && task.status !== options.status) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (options?.assignee && task.assignee !== options.assignee) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
if (options?.unassignedOnly && !!task.assignee) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
if (options?.readyOnly && !task.isReady) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
return true;
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
listMissionLog(limit?: number): MissionLogEntry[] {
|
|
604
|
+
if (!limit || limit <= 0) {
|
|
605
|
+
return [...this.missionLog];
|
|
606
|
+
}
|
|
607
|
+
return this.missionLog.slice(Math.max(0, this.missionLog.length - limit));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
listMailbox(
|
|
611
|
+
agentId: string,
|
|
612
|
+
options?: { unreadOnly?: boolean; markRead?: boolean; limit?: number },
|
|
613
|
+
): TeamMailboxMessage[] {
|
|
614
|
+
const unreadOnly = options?.unreadOnly ?? true;
|
|
615
|
+
const markRead = options?.markRead ?? true;
|
|
616
|
+
const limit = options?.limit;
|
|
617
|
+
const messages = this.mailbox.filter(
|
|
618
|
+
(message) =>
|
|
619
|
+
message.toAgentId === agentId && (!unreadOnly || !message.readAt),
|
|
620
|
+
);
|
|
621
|
+
const selected =
|
|
622
|
+
typeof limit === "number" && limit > 0
|
|
623
|
+
? messages.slice(Math.max(0, messages.length - limit))
|
|
624
|
+
: messages;
|
|
625
|
+
if (markRead) {
|
|
626
|
+
const now = new Date();
|
|
627
|
+
for (const message of selected) {
|
|
628
|
+
if (!message.readAt) {
|
|
629
|
+
message.readAt = now;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return selected.map((message) => ({ ...message }));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
getSnapshot(): TeamRuntimeSnapshot {
|
|
637
|
+
const taskCounts: Record<TeamTaskStatus, number> = {
|
|
638
|
+
pending: 0,
|
|
639
|
+
in_progress: 0,
|
|
640
|
+
blocked: 0,
|
|
641
|
+
completed: 0,
|
|
642
|
+
};
|
|
643
|
+
for (const task of this.tasks.values()) {
|
|
644
|
+
taskCounts[task.status]++;
|
|
645
|
+
}
|
|
646
|
+
const outcomeCounts: Record<TeamOutcomeStatus, number> = {
|
|
647
|
+
draft: 0,
|
|
648
|
+
in_review: 0,
|
|
649
|
+
finalized: 0,
|
|
650
|
+
};
|
|
651
|
+
for (const outcome of this.outcomes.values()) {
|
|
652
|
+
outcomeCounts[outcome.status]++;
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
teamId: this.teamId,
|
|
656
|
+
teamName: this.teamName,
|
|
657
|
+
members: Array.from(this.members.values()).map((member) => ({
|
|
658
|
+
agentId: member.agentId,
|
|
659
|
+
role: member.role,
|
|
660
|
+
description: member.description,
|
|
661
|
+
status: member.status,
|
|
662
|
+
})),
|
|
663
|
+
taskCounts,
|
|
664
|
+
unreadMessages: this.mailbox.filter((message) => !message.readAt).length,
|
|
665
|
+
missionLogEntries: this.missionLog.length,
|
|
666
|
+
activeRuns: Array.from(this.runs.values()).filter(
|
|
667
|
+
(run) => run.status === "running",
|
|
668
|
+
).length,
|
|
669
|
+
queuedRuns: Array.from(this.runs.values()).filter(
|
|
670
|
+
(run) => run.status === "queued",
|
|
671
|
+
).length,
|
|
672
|
+
outcomeCounts,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
exportState(): TeamRuntimeState {
|
|
677
|
+
return {
|
|
678
|
+
teamId: this.teamId,
|
|
679
|
+
teamName: this.teamName,
|
|
680
|
+
members: Array.from(this.members.values()).map((member) => ({
|
|
681
|
+
agentId: member.agentId,
|
|
682
|
+
role: member.role,
|
|
683
|
+
description: member.description,
|
|
684
|
+
status: member.status,
|
|
685
|
+
})),
|
|
686
|
+
tasks: Array.from(this.tasks.values()).map((task) => ({ ...task })),
|
|
687
|
+
mailbox: this.mailbox.map((message) => ({ ...message })),
|
|
688
|
+
missionLog: this.missionLog.map((entry) => ({ ...entry })),
|
|
689
|
+
runs: Array.from(this.runs.values()).map((run) => ({ ...run })),
|
|
690
|
+
outcomes: Array.from(this.outcomes.values()).map((outcome) => ({
|
|
691
|
+
...outcome,
|
|
692
|
+
})),
|
|
693
|
+
outcomeFragments: Array.from(this.outcomeFragments.values()).map(
|
|
694
|
+
(fragment) => ({ ...fragment }),
|
|
695
|
+
),
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
hydrateState(state: TeamRuntimeState): void {
|
|
700
|
+
this.tasks.clear();
|
|
701
|
+
for (const task of state.tasks) {
|
|
702
|
+
this.tasks.set(task.id, { ...task });
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
this.mailbox.length = 0;
|
|
706
|
+
this.mailbox.push(...state.mailbox.map((message) => ({ ...message })));
|
|
707
|
+
|
|
708
|
+
this.missionLog.length = 0;
|
|
709
|
+
this.missionLog.push(...state.missionLog.map((entry) => ({ ...entry })));
|
|
710
|
+
|
|
711
|
+
this.runs.clear();
|
|
712
|
+
for (const run of state.runs ?? []) {
|
|
713
|
+
this.runs.set(run.id, { ...run } as TeamRunRecord & {
|
|
714
|
+
result?: AgentResult;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
this.runQueue.length = 0;
|
|
718
|
+
this.runQueue.push(
|
|
719
|
+
...Array.from(this.runs.values())
|
|
720
|
+
.filter((run) => run.status === "queued")
|
|
721
|
+
.map((run) => run.id),
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
this.outcomes.clear();
|
|
725
|
+
for (const outcome of state.outcomes ?? []) {
|
|
726
|
+
this.outcomes.set(outcome.id, { ...outcome });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
this.outcomeFragments.clear();
|
|
730
|
+
for (const fragment of state.outcomeFragments ?? []) {
|
|
731
|
+
this.outcomeFragments.set(fragment.id, { ...fragment });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const leadMembers = Array.from(this.members.values()).filter(
|
|
735
|
+
(member) => member.role === "lead",
|
|
736
|
+
);
|
|
737
|
+
this.members.clear();
|
|
738
|
+
for (const lead of leadMembers) {
|
|
739
|
+
this.members.set(lead.agentId, {
|
|
740
|
+
...lead,
|
|
741
|
+
status: "idle",
|
|
742
|
+
runningCount: 0,
|
|
743
|
+
lastMissionStep: this.missionStepCounter,
|
|
744
|
+
lastMissionAt: Date.now(),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
for (const member of state.members) {
|
|
748
|
+
if (member.role !== "teammate") {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
this.members.set(member.agentId, {
|
|
752
|
+
agentId: member.agentId,
|
|
753
|
+
role: "teammate",
|
|
754
|
+
description: member.description,
|
|
755
|
+
status: "stopped",
|
|
756
|
+
agent: undefined,
|
|
757
|
+
runningCount: 0,
|
|
758
|
+
lastMissionStep: this.missionStepCounter,
|
|
759
|
+
lastMissionAt: Date.now(),
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
this.taskCounter = Math.max(
|
|
764
|
+
this.taskCounter,
|
|
765
|
+
maxCounter(
|
|
766
|
+
state.tasks.map((task) => task.id),
|
|
767
|
+
"task_",
|
|
768
|
+
),
|
|
769
|
+
);
|
|
770
|
+
this.messageCounter = Math.max(
|
|
771
|
+
this.messageCounter,
|
|
772
|
+
maxCounter(
|
|
773
|
+
state.mailbox.map((message) => message.id),
|
|
774
|
+
"msg_",
|
|
775
|
+
),
|
|
776
|
+
);
|
|
777
|
+
this.missionCounter = Math.max(
|
|
778
|
+
this.missionCounter,
|
|
779
|
+
maxCounter(
|
|
780
|
+
state.missionLog.map((entry) => entry.id),
|
|
781
|
+
"log_",
|
|
782
|
+
),
|
|
783
|
+
);
|
|
784
|
+
this.runCounter = Math.max(
|
|
785
|
+
this.runCounter,
|
|
786
|
+
maxCounter(
|
|
787
|
+
(state.runs ?? []).map((run) => run.id),
|
|
788
|
+
"run_",
|
|
789
|
+
),
|
|
790
|
+
);
|
|
791
|
+
this.outcomeCounter = Math.max(
|
|
792
|
+
this.outcomeCounter,
|
|
793
|
+
maxCounter(
|
|
794
|
+
(state.outcomes ?? []).map((outcome) => outcome.id),
|
|
795
|
+
"out_",
|
|
796
|
+
),
|
|
797
|
+
);
|
|
798
|
+
this.outcomeFragmentCounter = Math.max(
|
|
799
|
+
this.outcomeFragmentCounter,
|
|
800
|
+
maxCounter(
|
|
801
|
+
(state.outcomeFragments ?? []).map((fragment) => fragment.id),
|
|
802
|
+
"frag_",
|
|
803
|
+
),
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
isTeammateActive(agentId: string): boolean {
|
|
808
|
+
const member = this.members.get(agentId);
|
|
809
|
+
return !!member && member.role === "teammate" && !!member.agent;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
spawnTeammate({ agentId, config }: SpawnTeammateOptions): TeamMemberSnapshot {
|
|
813
|
+
const existing = this.members.get(agentId);
|
|
814
|
+
if (existing && existing.role !== "teammate") {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`Team member "${agentId}" already exists and is not a teammate`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
if (existing && existing.runningCount > 0) {
|
|
820
|
+
throw new Error(
|
|
821
|
+
`Teammate "${agentId}" is currently running and cannot be respawned`,
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const wrappedConfig: TeamMemberConfig = {
|
|
826
|
+
...config,
|
|
827
|
+
apiTimeoutMs: TEAMMATE_API_TIMEOUT_MS,
|
|
828
|
+
onEvent: (event: AgentEvent) => {
|
|
829
|
+
config.onEvent?.(event);
|
|
830
|
+
this.emitEvent({ type: TeamMessageType.AgentEvent, agentId, event });
|
|
831
|
+
this.trackMeaningfulEvent(agentId, event);
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const agent = createAgent(wrappedConfig);
|
|
836
|
+
const teammate: TeamMemberState = {
|
|
837
|
+
agentId,
|
|
838
|
+
role: "teammate",
|
|
839
|
+
description: config.role,
|
|
840
|
+
status: "idle",
|
|
841
|
+
agent,
|
|
842
|
+
runningCount: 0,
|
|
843
|
+
lastMissionStep: 0,
|
|
844
|
+
lastMissionAt: Date.now(),
|
|
845
|
+
};
|
|
846
|
+
this.members.set(agentId, teammate);
|
|
847
|
+
this.emitEvent({
|
|
848
|
+
type: TeamMessageType.TeammateSpawned,
|
|
849
|
+
agentId,
|
|
850
|
+
role: config.role,
|
|
851
|
+
teammate: {
|
|
852
|
+
rolePrompt: config.systemPrompt,
|
|
853
|
+
modelId: config.modelId,
|
|
854
|
+
maxIterations: config.maxIterations,
|
|
855
|
+
runtimeAgentId: agent.getAgentId(),
|
|
856
|
+
conversationId: agent.getConversationId(),
|
|
857
|
+
parentAgentId: null,
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
return {
|
|
861
|
+
agentId: teammate.agentId,
|
|
862
|
+
role: teammate.role,
|
|
863
|
+
description: teammate.description,
|
|
864
|
+
status: teammate.status,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
shutdownTeammate(agentId: string, reason?: string): void {
|
|
869
|
+
const member = this.members.get(agentId);
|
|
870
|
+
if (!member || member.role !== "teammate") {
|
|
871
|
+
throw new Error(`Teammate "${agentId}" was not found`);
|
|
872
|
+
}
|
|
873
|
+
member.agent?.abort();
|
|
874
|
+
member.status = "stopped";
|
|
875
|
+
this.emitEvent({ type: TeamMessageType.TeammateShutdown, agentId, reason });
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
updateTeammateConnections(
|
|
879
|
+
overrides: Partial<Pick<AgentConfig, "apiKey" | "baseUrl" | "headers">>,
|
|
880
|
+
): void {
|
|
881
|
+
for (const member of this.members.values()) {
|
|
882
|
+
if (member.role !== "teammate" || !member.agent) {
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
member.agent.updateConnection(overrides);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
createTask(input: CreateTeamTaskInput): TeamTask {
|
|
890
|
+
const taskId = `task_${String(++this.taskCounter).padStart(4, "0")}`;
|
|
891
|
+
const now = new Date();
|
|
892
|
+
const task: TeamTask = {
|
|
893
|
+
id: taskId,
|
|
894
|
+
title: input.title,
|
|
895
|
+
description: input.description,
|
|
896
|
+
status: input.assignee ? "in_progress" : "pending",
|
|
897
|
+
createdAt: now,
|
|
898
|
+
updatedAt: now,
|
|
899
|
+
createdBy: input.createdBy,
|
|
900
|
+
assignee: input.assignee,
|
|
901
|
+
dependsOn: input.dependsOn ?? [],
|
|
902
|
+
};
|
|
903
|
+
this.tasks.set(taskId, task);
|
|
904
|
+
this.emitEvent({
|
|
905
|
+
type: TeamMessageType.TeamTaskUpdated,
|
|
906
|
+
task: { ...task },
|
|
907
|
+
});
|
|
908
|
+
return { ...task };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
claimTask(taskId: string, agentId: string): TeamTask {
|
|
912
|
+
const task = this.requireTask(taskId);
|
|
913
|
+
this.assertDependenciesResolved(task);
|
|
914
|
+
task.status = "in_progress";
|
|
915
|
+
task.assignee = agentId;
|
|
916
|
+
task.updatedAt = new Date();
|
|
917
|
+
this.emitEvent({
|
|
918
|
+
type: TeamMessageType.TeamTaskUpdated,
|
|
919
|
+
task: { ...task },
|
|
920
|
+
});
|
|
921
|
+
this.appendMissionLog({
|
|
922
|
+
agentId,
|
|
923
|
+
taskId,
|
|
924
|
+
kind: "progress",
|
|
925
|
+
summary: `Claimed task "${task.title}"`,
|
|
926
|
+
});
|
|
927
|
+
return { ...task };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
blockTask(taskId: string, agentId: string, reason: string): TeamTask {
|
|
931
|
+
const task = this.requireTask(taskId);
|
|
932
|
+
task.status = "blocked";
|
|
933
|
+
task.updatedAt = new Date();
|
|
934
|
+
task.summary = reason;
|
|
935
|
+
this.emitEvent({
|
|
936
|
+
type: TeamMessageType.TeamTaskUpdated,
|
|
937
|
+
task: { ...task },
|
|
938
|
+
});
|
|
939
|
+
this.appendMissionLog({
|
|
940
|
+
agentId,
|
|
941
|
+
taskId,
|
|
942
|
+
kind: "blocked",
|
|
943
|
+
summary: reason,
|
|
944
|
+
});
|
|
945
|
+
return { ...task };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
completeTask(taskId: string, agentId: string, summary: string): TeamTask {
|
|
949
|
+
const task = this.requireTask(taskId);
|
|
950
|
+
task.status = "completed";
|
|
951
|
+
task.updatedAt = new Date();
|
|
952
|
+
task.summary = summary;
|
|
953
|
+
if (!task.assignee) {
|
|
954
|
+
task.assignee = agentId;
|
|
955
|
+
}
|
|
956
|
+
this.emitEvent({
|
|
957
|
+
type: TeamMessageType.TeamTaskUpdated,
|
|
958
|
+
task: { ...task },
|
|
959
|
+
});
|
|
960
|
+
this.appendMissionLog({
|
|
961
|
+
agentId,
|
|
962
|
+
taskId,
|
|
963
|
+
kind: "done",
|
|
964
|
+
summary,
|
|
965
|
+
});
|
|
966
|
+
return { ...task };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async routeToTeammate(
|
|
970
|
+
agentId: string,
|
|
971
|
+
message: string,
|
|
972
|
+
options?: RouteToTeammateOptions,
|
|
973
|
+
): Promise<AgentResult> {
|
|
974
|
+
const member = this.members.get(agentId);
|
|
975
|
+
if (!member || member.role !== "teammate" || !member.agent) {
|
|
976
|
+
throw new Error(`Teammate "${agentId}" was not found`);
|
|
977
|
+
}
|
|
978
|
+
if (!member.agent.canStartRun()) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
`Cannot start a new run while another run is already in progress`,
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
member.runningCount++;
|
|
985
|
+
member.status = "running";
|
|
986
|
+
this.emitEvent({ type: TeamMessageType.TaskStart, agentId, message });
|
|
987
|
+
|
|
988
|
+
try {
|
|
989
|
+
const result = options?.continueConversation
|
|
990
|
+
? await member.agent.continue(message)
|
|
991
|
+
: await member.agent.run(message);
|
|
992
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, result });
|
|
993
|
+
this.recordProgressStep(
|
|
994
|
+
agentId,
|
|
995
|
+
`Completed a delegated run (${result.iterations} iterations)`,
|
|
996
|
+
options?.taskId,
|
|
997
|
+
true,
|
|
998
|
+
);
|
|
999
|
+
return result;
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1002
|
+
this.emitEvent({
|
|
1003
|
+
type: TeamMessageType.TaskEnd,
|
|
1004
|
+
agentId,
|
|
1005
|
+
error: err,
|
|
1006
|
+
messages: member.agent.getMessages(),
|
|
1007
|
+
});
|
|
1008
|
+
this.appendMissionLog({
|
|
1009
|
+
agentId,
|
|
1010
|
+
taskId: options?.taskId,
|
|
1011
|
+
kind: "error",
|
|
1012
|
+
summary: err.message,
|
|
1013
|
+
});
|
|
1014
|
+
throw err;
|
|
1015
|
+
} finally {
|
|
1016
|
+
member.runningCount--;
|
|
1017
|
+
if (
|
|
1018
|
+
member.runningCount <= 0 &&
|
|
1019
|
+
this.members.get(agentId)?.status !== "stopped"
|
|
1020
|
+
) {
|
|
1021
|
+
member.status = "idle";
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
startTeammateRun(
|
|
1027
|
+
agentId: string,
|
|
1028
|
+
message: string,
|
|
1029
|
+
options?: RouteToTeammateOptions & {
|
|
1030
|
+
priority?: number;
|
|
1031
|
+
maxRetries?: number;
|
|
1032
|
+
leaseOwner?: string;
|
|
1033
|
+
},
|
|
1034
|
+
): TeamRunRecord {
|
|
1035
|
+
const runId = `run_${String(++this.runCounter).padStart(5, "0")}`;
|
|
1036
|
+
const record: TeamRunRecord & { result?: AgentResult } = {
|
|
1037
|
+
id: runId,
|
|
1038
|
+
agentId,
|
|
1039
|
+
taskId: options?.taskId,
|
|
1040
|
+
status: "queued",
|
|
1041
|
+
message,
|
|
1042
|
+
priority: options?.priority ?? 0,
|
|
1043
|
+
retryCount: 0,
|
|
1044
|
+
maxRetries: Math.max(0, options?.maxRetries ?? 0),
|
|
1045
|
+
continueConversation: options?.continueConversation,
|
|
1046
|
+
startedAt: new Date(0),
|
|
1047
|
+
leaseOwner: options?.leaseOwner,
|
|
1048
|
+
heartbeatAt: undefined,
|
|
1049
|
+
lastProgressAt: new Date(),
|
|
1050
|
+
lastProgressMessage: "queued",
|
|
1051
|
+
currentActivity: "queued",
|
|
1052
|
+
};
|
|
1053
|
+
this.runs.set(runId, record);
|
|
1054
|
+
this.runQueue.push(runId);
|
|
1055
|
+
this.emitEvent({ type: TeamMessageType.RunQueued, run: { ...record } });
|
|
1056
|
+
this.dispatchQueuedRuns();
|
|
1057
|
+
return { ...record };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private dispatchQueuedRuns(): void {
|
|
1061
|
+
while (
|
|
1062
|
+
this.countActiveRuns() < this.maxConcurrentRuns &&
|
|
1063
|
+
this.runQueue.length > 0
|
|
1064
|
+
) {
|
|
1065
|
+
const nextRunIndex = this.selectNextQueuedRunIndex();
|
|
1066
|
+
const [runId] = this.runQueue.splice(nextRunIndex, 1);
|
|
1067
|
+
const run = runId ? this.runs.get(runId) : undefined;
|
|
1068
|
+
if (!run || run.status !== "queued") {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
void this.executeQueuedRun(run);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
private selectNextQueuedRunIndex(): number {
|
|
1076
|
+
let selectedIndex = 0;
|
|
1077
|
+
let bestPriority = Number.NEGATIVE_INFINITY;
|
|
1078
|
+
for (let index = 0; index < this.runQueue.length; index++) {
|
|
1079
|
+
const run = this.runs.get(this.runQueue[index]);
|
|
1080
|
+
if (!run || run.status !== "queued") {
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
if (run.priority > bestPriority) {
|
|
1084
|
+
bestPriority = run.priority;
|
|
1085
|
+
selectedIndex = index;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return selectedIndex;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
private countActiveRuns(): number {
|
|
1092
|
+
let count = 0;
|
|
1093
|
+
for (const run of this.runs.values()) {
|
|
1094
|
+
if (run.status === "running") {
|
|
1095
|
+
count++;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return count;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
private async executeQueuedRun(
|
|
1102
|
+
run: TeamRunRecord & { result?: AgentResult },
|
|
1103
|
+
): Promise<void> {
|
|
1104
|
+
run.status = "running";
|
|
1105
|
+
run.startedAt = new Date();
|
|
1106
|
+
run.heartbeatAt = new Date();
|
|
1107
|
+
run.currentActivity = "run_started";
|
|
1108
|
+
this.emitEvent({ type: TeamMessageType.RunStarted, run: { ...run } });
|
|
1109
|
+
|
|
1110
|
+
const heartbeatTimer = setInterval(() => {
|
|
1111
|
+
if (run.status !== "running") {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
this.recordRunProgress(run, "heartbeat");
|
|
1115
|
+
}, 2000);
|
|
1116
|
+
|
|
1117
|
+
try {
|
|
1118
|
+
const result = await this.routeToTeammate(run.agentId, run.message, {
|
|
1119
|
+
taskId: run.taskId,
|
|
1120
|
+
continueConversation: run.continueConversation,
|
|
1121
|
+
});
|
|
1122
|
+
run.status = "completed";
|
|
1123
|
+
run.result = result;
|
|
1124
|
+
run.endedAt = new Date();
|
|
1125
|
+
run.currentActivity = "completed";
|
|
1126
|
+
this.emitEvent({ type: TeamMessageType.RunCompleted, run: { ...run } });
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
const message =
|
|
1129
|
+
error instanceof Error
|
|
1130
|
+
? error.message
|
|
1131
|
+
: String(error ?? "Unknown error");
|
|
1132
|
+
run.error = message;
|
|
1133
|
+
run.endedAt = new Date();
|
|
1134
|
+
if (run.retryCount < run.maxRetries) {
|
|
1135
|
+
run.retryCount++;
|
|
1136
|
+
run.status = "queued";
|
|
1137
|
+
run.nextAttemptAt = new Date(
|
|
1138
|
+
Date.now() + Math.min(30000, 1000 * 2 ** run.retryCount),
|
|
1139
|
+
);
|
|
1140
|
+
this.runQueue.push(run.id);
|
|
1141
|
+
this.recordRunProgress(run, `retry_scheduled_${run.retryCount}`);
|
|
1142
|
+
} else {
|
|
1143
|
+
run.status = "failed";
|
|
1144
|
+
run.currentActivity = "failed";
|
|
1145
|
+
this.emitEvent({ type: TeamMessageType.RunFailed, run: { ...run } });
|
|
1146
|
+
}
|
|
1147
|
+
} finally {
|
|
1148
|
+
clearInterval(heartbeatTimer);
|
|
1149
|
+
this.dispatchQueuedRuns();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
listRuns(options?: {
|
|
1154
|
+
status?: TeamRunStatus | null;
|
|
1155
|
+
agentId?: string | null;
|
|
1156
|
+
includeCompleted?: boolean | null;
|
|
1157
|
+
}): TeamRunRecord[] {
|
|
1158
|
+
const includeCompleted = options?.includeCompleted ?? true;
|
|
1159
|
+
return Array.from(this.runs.values())
|
|
1160
|
+
.filter((run) => {
|
|
1161
|
+
if (!includeCompleted && !["running", "queued"].includes(run.status)) {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
if (options?.status && run.status !== options.status) {
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
if (options?.agentId && run.agentId !== options.agentId) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
return true;
|
|
1171
|
+
})
|
|
1172
|
+
.map((run) => ({ ...run }));
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
getRun(runId: string): TeamRunRecord | undefined {
|
|
1176
|
+
const run = this.runs.get(runId);
|
|
1177
|
+
return run ? { ...run } : undefined;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
async awaitRun(runId: string, pollIntervalMs = 250): Promise<TeamRunRecord> {
|
|
1181
|
+
const run = this.runs.get(runId);
|
|
1182
|
+
if (!run) {
|
|
1183
|
+
throw new Error(`Run "${runId}" was not found`);
|
|
1184
|
+
}
|
|
1185
|
+
while (run.status === "running") {
|
|
1186
|
+
await sleep(pollIntervalMs);
|
|
1187
|
+
}
|
|
1188
|
+
return { ...run };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
async awaitAllRuns(pollIntervalMs = 250): Promise<TeamRunRecord[]> {
|
|
1192
|
+
while (
|
|
1193
|
+
Array.from(this.runs.values()).some((run) =>
|
|
1194
|
+
["queued", "running"].includes(run.status),
|
|
1195
|
+
)
|
|
1196
|
+
) {
|
|
1197
|
+
await sleep(pollIntervalMs);
|
|
1198
|
+
}
|
|
1199
|
+
return this.listRuns();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
cancelRun(runId: string, reason?: string): TeamRunRecord {
|
|
1203
|
+
const run = this.runs.get(runId);
|
|
1204
|
+
if (!run) {
|
|
1205
|
+
throw new Error(`Run "${runId}" was not found`);
|
|
1206
|
+
}
|
|
1207
|
+
if (run.status === "completed" || run.status === "failed") {
|
|
1208
|
+
return { ...run };
|
|
1209
|
+
}
|
|
1210
|
+
run.status = "cancelled";
|
|
1211
|
+
run.error = reason;
|
|
1212
|
+
run.endedAt = new Date();
|
|
1213
|
+
run.currentActivity = "cancelled";
|
|
1214
|
+
const queueIndex = this.runQueue.indexOf(runId);
|
|
1215
|
+
if (queueIndex >= 0) {
|
|
1216
|
+
this.runQueue.splice(queueIndex, 1);
|
|
1217
|
+
}
|
|
1218
|
+
this.emitEvent({
|
|
1219
|
+
type: TeamMessageType.RunCancelled,
|
|
1220
|
+
run: { ...run },
|
|
1221
|
+
reason,
|
|
1222
|
+
});
|
|
1223
|
+
return { ...run };
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
markStaleRunsInterrupted(reason = "runtime_recovered"): TeamRunRecord[] {
|
|
1227
|
+
const interrupted: TeamRunRecord[] = [];
|
|
1228
|
+
for (const run of this.runs.values()) {
|
|
1229
|
+
if (!["queued", "running"].includes(run.status)) {
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
run.status = "interrupted";
|
|
1233
|
+
run.error = reason;
|
|
1234
|
+
run.endedAt = new Date();
|
|
1235
|
+
run.currentActivity = "interrupted";
|
|
1236
|
+
interrupted.push({ ...run });
|
|
1237
|
+
this.emitEvent({
|
|
1238
|
+
type: TeamMessageType.RunInterrupted,
|
|
1239
|
+
run: { ...run },
|
|
1240
|
+
reason,
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
this.runQueue.length = 0;
|
|
1244
|
+
return interrupted;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
sendMessage(
|
|
1248
|
+
fromAgentId: string,
|
|
1249
|
+
toAgentId: string,
|
|
1250
|
+
subject: string,
|
|
1251
|
+
body: string,
|
|
1252
|
+
taskId?: string,
|
|
1253
|
+
): TeamMailboxMessage {
|
|
1254
|
+
if (!this.members.has(fromAgentId)) {
|
|
1255
|
+
throw new Error(`Unknown sender "${fromAgentId}"`);
|
|
1256
|
+
}
|
|
1257
|
+
if (!this.members.has(toAgentId)) {
|
|
1258
|
+
throw new Error(`Unknown recipient "${toAgentId}"`);
|
|
1259
|
+
}
|
|
1260
|
+
const message: TeamMailboxMessage = {
|
|
1261
|
+
id: `msg_${String(++this.messageCounter).padStart(5, "0")}`,
|
|
1262
|
+
teamId: this.teamId,
|
|
1263
|
+
fromAgentId,
|
|
1264
|
+
toAgentId,
|
|
1265
|
+
subject,
|
|
1266
|
+
body,
|
|
1267
|
+
taskId,
|
|
1268
|
+
sentAt: new Date(),
|
|
1269
|
+
};
|
|
1270
|
+
this.mailbox.push(message);
|
|
1271
|
+
this.emitEvent({
|
|
1272
|
+
type: TeamMessageType.TeamMessage,
|
|
1273
|
+
message: { ...message },
|
|
1274
|
+
});
|
|
1275
|
+
return { ...message };
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
broadcast(
|
|
1279
|
+
fromAgentId: string,
|
|
1280
|
+
subject: string,
|
|
1281
|
+
body: string,
|
|
1282
|
+
options?: { includeLead?: boolean; taskId?: string },
|
|
1283
|
+
): TeamMailboxMessage[] {
|
|
1284
|
+
const includeLead = options?.includeLead ?? false;
|
|
1285
|
+
const messages: TeamMailboxMessage[] = [];
|
|
1286
|
+
for (const member of this.members.values()) {
|
|
1287
|
+
if (member.agentId === fromAgentId) {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
if (!includeLead && member.role === "lead") {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
messages.push(
|
|
1294
|
+
this.sendMessage(
|
|
1295
|
+
fromAgentId,
|
|
1296
|
+
member.agentId,
|
|
1297
|
+
subject,
|
|
1298
|
+
body,
|
|
1299
|
+
options?.taskId,
|
|
1300
|
+
),
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
return messages;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
appendMissionLog(input: AppendMissionLogInput): MissionLogEntry {
|
|
1307
|
+
if (!this.members.has(input.agentId)) {
|
|
1308
|
+
throw new Error(`Unknown team member "${input.agentId}"`);
|
|
1309
|
+
}
|
|
1310
|
+
const entry: MissionLogEntry = {
|
|
1311
|
+
id: `log_${String(++this.missionCounter).padStart(6, "0")}`,
|
|
1312
|
+
ts: new Date(),
|
|
1313
|
+
teamId: this.teamId,
|
|
1314
|
+
agentId: input.agentId,
|
|
1315
|
+
taskId: input.taskId,
|
|
1316
|
+
kind: input.kind,
|
|
1317
|
+
summary: input.summary,
|
|
1318
|
+
evidence: input.evidence,
|
|
1319
|
+
nextAction: input.nextAction,
|
|
1320
|
+
};
|
|
1321
|
+
this.missionLog.push(entry);
|
|
1322
|
+
const member = this.members.get(input.agentId);
|
|
1323
|
+
if (member) {
|
|
1324
|
+
member.lastMissionAt = Date.now();
|
|
1325
|
+
member.lastMissionStep = this.missionStepCounter;
|
|
1326
|
+
}
|
|
1327
|
+
this.emitEvent({
|
|
1328
|
+
type: TeamMessageType.TeamMissionLog,
|
|
1329
|
+
entry: { ...entry },
|
|
1330
|
+
});
|
|
1331
|
+
return { ...entry };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
createOutcome(input: CreateTeamOutcomeInput): TeamOutcome {
|
|
1335
|
+
const outcome: TeamOutcome = {
|
|
1336
|
+
id: `out_${String(++this.outcomeCounter).padStart(4, "0")}`,
|
|
1337
|
+
teamId: this.teamId,
|
|
1338
|
+
title: input.title,
|
|
1339
|
+
status: "draft",
|
|
1340
|
+
requiredSections: [...new Set(input.requiredSections)],
|
|
1341
|
+
createdBy: input.createdBy,
|
|
1342
|
+
createdAt: new Date(),
|
|
1343
|
+
};
|
|
1344
|
+
this.outcomes.set(outcome.id, outcome);
|
|
1345
|
+
this.emitEvent({
|
|
1346
|
+
type: TeamMessageType.OutcomeCreated,
|
|
1347
|
+
outcome: { ...outcome },
|
|
1348
|
+
});
|
|
1349
|
+
return { ...outcome };
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
listOutcomes(): TeamOutcome[] {
|
|
1353
|
+
return Array.from(this.outcomes.values()).map((outcome) => ({
|
|
1354
|
+
...outcome,
|
|
1355
|
+
}));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
attachOutcomeFragment(
|
|
1359
|
+
input: AttachTeamOutcomeFragmentInput,
|
|
1360
|
+
): TeamOutcomeFragment {
|
|
1361
|
+
const outcome = this.outcomes.get(input.outcomeId);
|
|
1362
|
+
if (!outcome) {
|
|
1363
|
+
throw new Error(`Outcome "${input.outcomeId}" was not found`);
|
|
1364
|
+
}
|
|
1365
|
+
if (!outcome.requiredSections.includes(input.section)) {
|
|
1366
|
+
throw new Error(
|
|
1367
|
+
`Section "${input.section}" is not part of outcome "${input.outcomeId}"`,
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
const fragment: TeamOutcomeFragment = {
|
|
1371
|
+
id: `frag_${String(++this.outcomeFragmentCounter).padStart(5, "0")}`,
|
|
1372
|
+
teamId: this.teamId,
|
|
1373
|
+
outcomeId: input.outcomeId,
|
|
1374
|
+
section: input.section,
|
|
1375
|
+
sourceAgentId: input.sourceAgentId,
|
|
1376
|
+
sourceRunId: input.sourceRunId,
|
|
1377
|
+
content: input.content,
|
|
1378
|
+
status: "draft",
|
|
1379
|
+
createdAt: new Date(),
|
|
1380
|
+
};
|
|
1381
|
+
this.outcomeFragments.set(fragment.id, fragment);
|
|
1382
|
+
if (outcome.status === "draft") {
|
|
1383
|
+
outcome.status = "in_review";
|
|
1384
|
+
}
|
|
1385
|
+
this.emitEvent({
|
|
1386
|
+
type: TeamMessageType.OutcomeFragmentAttached,
|
|
1387
|
+
fragment: { ...fragment },
|
|
1388
|
+
});
|
|
1389
|
+
return { ...fragment };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
reviewOutcomeFragment(
|
|
1393
|
+
input: ReviewTeamOutcomeFragmentInput,
|
|
1394
|
+
): TeamOutcomeFragment {
|
|
1395
|
+
const fragment = this.outcomeFragments.get(input.fragmentId);
|
|
1396
|
+
if (!fragment) {
|
|
1397
|
+
throw new Error(`Fragment "${input.fragmentId}" was not found`);
|
|
1398
|
+
}
|
|
1399
|
+
fragment.status = input.approved ? "reviewed" : "rejected";
|
|
1400
|
+
fragment.reviewedBy = input.reviewedBy;
|
|
1401
|
+
fragment.reviewedAt = new Date();
|
|
1402
|
+
this.emitEvent({
|
|
1403
|
+
type: TeamMessageType.OutcomeFragmentReviewed,
|
|
1404
|
+
fragment: { ...fragment },
|
|
1405
|
+
});
|
|
1406
|
+
return { ...fragment };
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
listOutcomeFragments(outcomeId: string): TeamOutcomeFragment[] {
|
|
1410
|
+
return Array.from(this.outcomeFragments.values())
|
|
1411
|
+
.filter((fragment) => fragment.outcomeId === outcomeId)
|
|
1412
|
+
.map((fragment) => ({ ...fragment }));
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
finalizeOutcome(outcomeId: string): TeamOutcome {
|
|
1416
|
+
const outcome = this.outcomes.get(outcomeId);
|
|
1417
|
+
if (!outcome) {
|
|
1418
|
+
throw new Error(`Outcome "${outcomeId}" was not found`);
|
|
1419
|
+
}
|
|
1420
|
+
const fragments = this.listOutcomeFragments(outcomeId);
|
|
1421
|
+
for (const section of outcome.requiredSections) {
|
|
1422
|
+
const approvedForSection = fragments.some(
|
|
1423
|
+
(fragment) =>
|
|
1424
|
+
fragment.section === section && fragment.status === "reviewed",
|
|
1425
|
+
);
|
|
1426
|
+
if (!approvedForSection) {
|
|
1427
|
+
throw new Error(
|
|
1428
|
+
`Outcome "${outcomeId}" cannot be finalized. Section "${section}" is missing a reviewed fragment.`,
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
outcome.status = "finalized";
|
|
1433
|
+
outcome.finalizedAt = new Date();
|
|
1434
|
+
this.emitEvent({
|
|
1435
|
+
type: TeamMessageType.OutcomeFinalized,
|
|
1436
|
+
outcome: { ...outcome },
|
|
1437
|
+
});
|
|
1438
|
+
return { ...outcome };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
cleanup(): void {
|
|
1442
|
+
for (const member of this.members.values()) {
|
|
1443
|
+
if (member.role === "teammate" && member.runningCount > 0) {
|
|
1444
|
+
throw new Error(
|
|
1445
|
+
`Cannot cleanup team while teammate "${member.agentId}" is still running`,
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (
|
|
1450
|
+
Array.from(this.runs.values()).some((run) =>
|
|
1451
|
+
["queued", "running"].includes(run.status),
|
|
1452
|
+
)
|
|
1453
|
+
) {
|
|
1454
|
+
throw new Error(
|
|
1455
|
+
"Cannot cleanup team while async teammate runs are still active",
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
for (const member of this.members.values()) {
|
|
1460
|
+
if (member.role === "teammate") {
|
|
1461
|
+
member.agent?.abort();
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
this.tasks.clear();
|
|
1466
|
+
this.mailbox.length = 0;
|
|
1467
|
+
this.missionLog.length = 0;
|
|
1468
|
+
this.runs.clear();
|
|
1469
|
+
this.runQueue.length = 0;
|
|
1470
|
+
this.outcomes.clear();
|
|
1471
|
+
this.outcomeFragments.clear();
|
|
1472
|
+
|
|
1473
|
+
for (const [memberId, member] of this.members.entries()) {
|
|
1474
|
+
if (member.role === "teammate") {
|
|
1475
|
+
this.members.delete(memberId);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private requireTask(taskId: string): TeamTask {
|
|
1481
|
+
const task = this.tasks.get(taskId);
|
|
1482
|
+
if (!task) {
|
|
1483
|
+
throw new Error(`Task "${taskId}" was not found`);
|
|
1484
|
+
}
|
|
1485
|
+
return task;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
private assertDependenciesResolved(task: TeamTask): void {
|
|
1489
|
+
const blockedBy = this.getUnresolvedDependencies(task);
|
|
1490
|
+
if (blockedBy.length > 0) {
|
|
1491
|
+
throw new Error(`Task "${task.id}" is blocked by "${blockedBy[0]}"`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
private getUnresolvedDependencies(task: TeamTask): string[] {
|
|
1496
|
+
return task.dependsOn.filter((dependencyId) => {
|
|
1497
|
+
const dependency = this.tasks.get(dependencyId);
|
|
1498
|
+
return !dependency || dependency.status !== "completed";
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
private trackMeaningfulEvent(agentId: string, event: AgentEvent): void {
|
|
1503
|
+
this.recordRunActivityFromAgentEvent(agentId, event);
|
|
1504
|
+
|
|
1505
|
+
if (event.type === "iteration_end" && event.hadToolCalls) {
|
|
1506
|
+
this.recordProgressStep(
|
|
1507
|
+
agentId,
|
|
1508
|
+
`Completed iteration ${event.iteration} with ${event.toolCallCount} tool call(s)`,
|
|
1509
|
+
);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (
|
|
1514
|
+
event.type === "content_end" &&
|
|
1515
|
+
event.contentType === "tool" &&
|
|
1516
|
+
!event.error
|
|
1517
|
+
) {
|
|
1518
|
+
this.recordProgressStep(
|
|
1519
|
+
agentId,
|
|
1520
|
+
`Finished tool "${event.toolName ?? "unknown"}"`,
|
|
1521
|
+
);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (event.type === "done") {
|
|
1526
|
+
this.appendMissionLog({
|
|
1527
|
+
agentId,
|
|
1528
|
+
kind: "done",
|
|
1529
|
+
summary: `Run completed after ${event.iterations} iteration(s)`,
|
|
1530
|
+
});
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
if (event.type === "error") {
|
|
1535
|
+
this.appendMissionLog({
|
|
1536
|
+
agentId,
|
|
1537
|
+
kind: "error",
|
|
1538
|
+
summary: event.error.message,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
private recordRunActivityFromAgentEvent(
|
|
1544
|
+
agentId: string,
|
|
1545
|
+
event: AgentEvent,
|
|
1546
|
+
): void {
|
|
1547
|
+
let activity: string | undefined;
|
|
1548
|
+
switch (event.type) {
|
|
1549
|
+
case "iteration_start":
|
|
1550
|
+
activity = `iteration_${event.iteration}_started`;
|
|
1551
|
+
break;
|
|
1552
|
+
case "content_start":
|
|
1553
|
+
if (event.contentType === "tool") {
|
|
1554
|
+
activity = `running_tool_${event.toolName ?? "unknown"}`;
|
|
1555
|
+
}
|
|
1556
|
+
break;
|
|
1557
|
+
case "content_end":
|
|
1558
|
+
if (event.contentType === "tool") {
|
|
1559
|
+
activity = event.error
|
|
1560
|
+
? this.formatProgressErrorActivity(
|
|
1561
|
+
`tool_${event.toolName ?? "unknown"}_error`,
|
|
1562
|
+
event.error,
|
|
1563
|
+
)
|
|
1564
|
+
: `finished_tool_${event.toolName ?? "unknown"}`;
|
|
1565
|
+
}
|
|
1566
|
+
break;
|
|
1567
|
+
case "done":
|
|
1568
|
+
activity = "finalizing_response";
|
|
1569
|
+
break;
|
|
1570
|
+
case "error":
|
|
1571
|
+
activity = this.formatProgressErrorActivity(
|
|
1572
|
+
"run_error",
|
|
1573
|
+
event.error.message,
|
|
1574
|
+
);
|
|
1575
|
+
break;
|
|
1576
|
+
default:
|
|
1577
|
+
break;
|
|
1578
|
+
}
|
|
1579
|
+
if (!activity) {
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
for (const run of this.runs.values()) {
|
|
1583
|
+
if (run.agentId !== agentId || run.status !== "running") {
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
this.recordRunProgress(run, activity);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
private recordRunProgress(run: TeamRunRecord, message: string): void {
|
|
1591
|
+
const now = new Date();
|
|
1592
|
+
run.heartbeatAt = now;
|
|
1593
|
+
run.lastProgressAt = now;
|
|
1594
|
+
run.lastProgressMessage = message;
|
|
1595
|
+
run.currentActivity = message;
|
|
1596
|
+
this.emitEvent({
|
|
1597
|
+
type: TeamMessageType.RunProgress,
|
|
1598
|
+
run: { ...run },
|
|
1599
|
+
message,
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
private formatProgressErrorActivity(prefix: string, detail: string): string {
|
|
1604
|
+
const summary = detail.replace(/\s+/g, " ").trim();
|
|
1605
|
+
if (summary.length === 0) {
|
|
1606
|
+
return prefix;
|
|
1607
|
+
}
|
|
1608
|
+
const suffix =
|
|
1609
|
+
summary.length > 240 ? `${summary.slice(0, 237).trimEnd()}...` : summary;
|
|
1610
|
+
return `${prefix}: ${suffix}`;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
private recordProgressStep(
|
|
1614
|
+
agentId: string,
|
|
1615
|
+
summary: string,
|
|
1616
|
+
taskId?: string,
|
|
1617
|
+
force = false,
|
|
1618
|
+
): void {
|
|
1619
|
+
this.missionStepCounter++;
|
|
1620
|
+
const member = this.members.get(agentId);
|
|
1621
|
+
if (!member) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const stepsSinceLast = this.missionStepCounter - member.lastMissionStep;
|
|
1625
|
+
const elapsedMs = Date.now() - member.lastMissionAt;
|
|
1626
|
+
if (
|
|
1627
|
+
!force &&
|
|
1628
|
+
stepsSinceLast < this.missionLogIntervalSteps &&
|
|
1629
|
+
elapsedMs < this.missionLogIntervalMs
|
|
1630
|
+
) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
this.appendMissionLog({
|
|
1634
|
+
agentId,
|
|
1635
|
+
taskId,
|
|
1636
|
+
kind: "progress",
|
|
1637
|
+
summary,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
private emitEvent(event: TeamEvent): void {
|
|
1642
|
+
try {
|
|
1643
|
+
this.onTeamEvent?.(event);
|
|
1644
|
+
} catch {
|
|
1645
|
+
// Ignore callback errors to avoid disrupting execution.
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function sleep(ms: number): Promise<void> {
|
|
1651
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function maxCounter(ids: string[], prefix: string): number {
|
|
1655
|
+
let max = 0;
|
|
1656
|
+
for (const id of ids) {
|
|
1657
|
+
if (!id.startsWith(prefix)) {
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
const value = Number.parseInt(id.slice(prefix.length), 10);
|
|
1661
|
+
if (Number.isFinite(value)) {
|
|
1662
|
+
max = Math.max(max, value);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return max;
|
|
1666
|
+
}
|