pi-crew 0.2.3 → 0.2.4

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.
Files changed (348) hide show
  1. package/AGENTS.md +57 -32
  2. package/CHANGELOG.md +466 -448
  3. package/LICENSE +21 -21
  4. package/NOTICE.md +16 -16
  5. package/README.md +323 -323
  6. package/docs/FEATURE_INTAKE.md +126 -0
  7. package/docs/HARNESS.md +86 -0
  8. package/docs/HARNESS_BACKLOG.md +41 -0
  9. package/docs/TEST_MATRIX.md +49 -0
  10. package/docs/actions-reference.md +595 -595
  11. package/docs/architecture.md +180 -180
  12. package/docs/code-review-2026-05-11.md +592 -592
  13. package/docs/commands-reference.md +347 -347
  14. package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
  15. package/docs/decisions/0001-durable-state.md +41 -0
  16. package/docs/decisions/0002-child-process-for-async.md +42 -0
  17. package/docs/decisions/0003-depth-guard.md +36 -0
  18. package/docs/decisions/0004-execfile-over-exec.md +34 -0
  19. package/docs/decisions/0005-no-parameter-properties.md +49 -0
  20. package/docs/decisions/0006-publish-bundled-esm.md +63 -0
  21. package/docs/decisions/0007-active-run-binary-index.md +54 -0
  22. package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
  23. package/docs/decisions/README.md +23 -0
  24. package/docs/followup-review-round4-2026-05-13.md +107 -0
  25. package/docs/implementation-plan-top3.md +333 -0
  26. package/docs/live-mailbox-runtime.md +36 -36
  27. package/docs/next-upgrade-roadmap.md +808 -808
  28. package/docs/oh-my-pi-research.md +509 -0
  29. package/docs/perf/baseline-2026-05.md +113 -0
  30. package/docs/perf/final-report-2026-05.md +206 -0
  31. package/docs/perf/sprint-1-report.md +71 -0
  32. package/docs/perf/sprint-2-report.md +81 -0
  33. package/docs/perf/sprint-2.5-report.md +53 -0
  34. package/docs/perf/sprint-3-report.md +36 -0
  35. package/docs/perf/sprint-4-report.md +47 -0
  36. package/docs/perf/sprint-5-report.md +51 -0
  37. package/docs/perf/sprint-6-report.md +94 -0
  38. package/docs/perf/sprint-7-report.md +74 -0
  39. package/docs/perf/upgrade-plan-2026-05.md +147 -0
  40. package/docs/pi-subagents3-deep-analysis.md +508 -0
  41. package/docs/product/README.md +31 -0
  42. package/docs/product/platform.md +27 -0
  43. package/docs/product/runtime-safety.md +37 -0
  44. package/docs/product/team-run.md +39 -0
  45. package/docs/product/team-tool.md +37 -0
  46. package/docs/publishing.md +65 -65
  47. package/docs/resource-formats.md +134 -134
  48. package/docs/runtime-analysis-child-vs-live.md +171 -0
  49. package/docs/runtime-flow.md +148 -148
  50. package/docs/runtime-migration-in-process-analysis.md +250 -0
  51. package/docs/stories/README.md +30 -0
  52. package/docs/stories/backlog.md +36 -0
  53. package/docs/templates/decision.md +27 -0
  54. package/docs/templates/story.md +44 -0
  55. package/docs/templates/validation-report.md +32 -0
  56. package/docs/usage.md +238 -238
  57. package/index.ts +7 -6
  58. package/install.mjs +65 -65
  59. package/package.json +107 -100
  60. package/schema.json +222 -222
  61. package/skills/child-pi-spawning/SKILL.md +213 -0
  62. package/skills/context-artifact-hygiene/SKILL.md +32 -0
  63. package/skills/event-log-tracing/SKILL.md +299 -0
  64. package/skills/git-master/SKILL.md +225 -24
  65. package/skills/live-agent-lifecycle/SKILL.md +192 -0
  66. package/skills/mailbox-interactive/SKILL.md +300 -19
  67. package/skills/model-routing-context/SKILL.md +94 -0
  68. package/skills/multi-perspective-review/SKILL.md +88 -0
  69. package/skills/read-only-explorer/SKILL.md +250 -26
  70. package/skills/safe-bash/SKILL.md +307 -21
  71. package/skills/verification-before-done/SKILL.md +11 -2
  72. package/skills/widget-rendering/SKILL.md +258 -0
  73. package/skills/workspace-isolation/SKILL.md +202 -0
  74. package/skills/worktree-isolation/SKILL.md +202 -18
  75. package/src/adapters/claude-adapter.ts +25 -25
  76. package/src/adapters/codex-adapter.ts +21 -21
  77. package/src/adapters/cursor-adapter.ts +17 -17
  78. package/src/adapters/export-util.ts +137 -137
  79. package/src/adapters/index.ts +15 -15
  80. package/src/adapters/registry.ts +18 -18
  81. package/src/adapters/types.ts +23 -23
  82. package/src/agents/agent-config.ts +38 -38
  83. package/src/agents/agent-serializer.ts +38 -38
  84. package/src/agents/discover-agents.ts +121 -118
  85. package/src/config/config.ts +740 -858
  86. package/src/config/defaults.ts +96 -96
  87. package/src/config/drift-detector.ts +211 -211
  88. package/src/config/markers.ts +327 -327
  89. package/src/config/resilient-parser.ts +109 -108
  90. package/src/config/suggestions.ts +74 -74
  91. package/src/config/types.ts +199 -0
  92. package/src/extension/async-notifier.ts +123 -89
  93. package/src/extension/autonomous-policy.ts +169 -169
  94. package/src/extension/cross-extension-rpc.ts +104 -104
  95. package/src/extension/help.ts +47 -47
  96. package/src/extension/import-index.ts +69 -69
  97. package/src/extension/management.ts +395 -382
  98. package/src/extension/notification-router.ts +116 -116
  99. package/src/extension/notification-sink.ts +51 -51
  100. package/src/extension/project-init.ts +168 -168
  101. package/src/extension/register.ts +859 -668
  102. package/src/extension/registration/artifact-cleanup.ts +15 -15
  103. package/src/extension/registration/command-utils.ts +54 -54
  104. package/src/extension/registration/commands.ts +559 -452
  105. package/src/extension/registration/compaction-guard.ts +125 -125
  106. package/src/extension/registration/subagent-helpers.ts +102 -102
  107. package/src/extension/registration/subagent-tools.ts +220 -159
  108. package/src/extension/registration/team-tool.ts +159 -99
  109. package/src/extension/registration/viewers.ts +29 -0
  110. package/src/extension/result-watcher.ts +128 -128
  111. package/src/extension/run-bundle-schema.ts +89 -89
  112. package/src/extension/run-export.ts +73 -73
  113. package/src/extension/run-import.ts +84 -84
  114. package/src/extension/run-index.ts +94 -94
  115. package/src/extension/run-maintenance.ts +142 -142
  116. package/src/extension/session-summary.ts +8 -8
  117. package/src/extension/team-manager-command.ts +96 -96
  118. package/src/extension/team-recommendation.ts +188 -188
  119. package/src/extension/team-tool/api.ts +5 -2
  120. package/src/extension/team-tool/cancel.ts +224 -209
  121. package/src/extension/team-tool/config-patch.ts +36 -36
  122. package/src/extension/team-tool/context.ts +60 -60
  123. package/src/extension/team-tool/doctor.ts +242 -242
  124. package/src/extension/team-tool/handle-settings.ts +421 -195
  125. package/src/extension/team-tool/inspect.ts +41 -41
  126. package/src/extension/team-tool/lifecycle-actions.ts +139 -139
  127. package/src/extension/team-tool/parallel-dispatch.ts +156 -156
  128. package/src/extension/team-tool/plan.ts +19 -19
  129. package/src/extension/team-tool/respond.ts +112 -111
  130. package/src/extension/team-tool/run.ts +246 -229
  131. package/src/extension/team-tool/status.ts +110 -110
  132. package/src/extension/team-tool-types.ts +13 -13
  133. package/src/extension/team-tool.ts +344 -344
  134. package/src/extension/tool-result.ts +16 -16
  135. package/src/extension/validate-resources.ts +77 -77
  136. package/src/hooks/registry.ts +61 -61
  137. package/src/hooks/types.ts +40 -40
  138. package/src/i18n.ts +184 -184
  139. package/src/observability/correlation.ts +35 -35
  140. package/src/observability/event-to-metric.ts +68 -68
  141. package/src/observability/exporters/adapter.ts +30 -30
  142. package/src/observability/exporters/otlp-exporter.ts +106 -92
  143. package/src/observability/exporters/prometheus-exporter.ts +54 -54
  144. package/src/observability/metric-registry.ts +87 -87
  145. package/src/observability/metric-retention.ts +54 -54
  146. package/src/observability/metric-sink.ts +81 -56
  147. package/src/observability/metrics-primitives.ts +167 -167
  148. package/src/prompt/prompt-runtime.ts +72 -72
  149. package/src/runtime/adaptive-plan.ts +338 -0
  150. package/src/runtime/agent-control.ts +169 -169
  151. package/src/runtime/agent-memory.ts +72 -72
  152. package/src/runtime/agent-observability.ts +114 -114
  153. package/src/runtime/async-marker.ts +26 -26
  154. package/src/runtime/async-runner.ts +153 -153
  155. package/src/runtime/attention-events.ts +28 -28
  156. package/src/runtime/auto-resume.ts +100 -100
  157. package/src/runtime/background-runner.ts +122 -89
  158. package/src/runtime/cancellation.ts +61 -61
  159. package/src/runtime/capability-inventory.ts +116 -116
  160. package/src/runtime/child-pi-pool.ts +68 -0
  161. package/src/runtime/child-pi.ts +541 -461
  162. package/src/runtime/code-summary.ts +247 -247
  163. package/src/runtime/compaction-summary.ts +271 -271
  164. package/src/runtime/concurrency.ts +58 -58
  165. package/src/runtime/crash-recovery.ts +317 -301
  166. package/src/runtime/crew-agent-records.ts +379 -281
  167. package/src/runtime/crew-agent-runtime.ts +60 -60
  168. package/src/runtime/cross-extension-rpc.ts +72 -0
  169. package/src/runtime/custom-tools/irc-tool.ts +201 -201
  170. package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
  171. package/src/runtime/deadletter.ts +47 -47
  172. package/src/runtime/delivery-coordinator.ts +176 -176
  173. package/src/runtime/delta-conflict.ts +360 -360
  174. package/src/runtime/diagnostic-export.ts +102 -102
  175. package/src/runtime/direct-run.ts +35 -35
  176. package/src/runtime/effectiveness.ts +82 -81
  177. package/src/runtime/errors/crew-errors.ts +166 -0
  178. package/src/runtime/event-stream-bridge.ts +92 -92
  179. package/src/runtime/foreground-control.ts +82 -82
  180. package/src/runtime/green-contract.ts +46 -46
  181. package/src/runtime/group-join.ts +234 -106
  182. package/src/runtime/heartbeat-watcher.ts +145 -124
  183. package/src/runtime/iteration-hooks.ts +267 -267
  184. package/src/runtime/live-agent-control.ts +88 -88
  185. package/src/runtime/live-agent-manager.ts +377 -179
  186. package/src/runtime/live-control-realtime.ts +36 -36
  187. package/src/runtime/live-session-runtime.ts +676 -600
  188. package/src/runtime/loop-gates.ts +129 -129
  189. package/src/runtime/manifest-cache.ts +263 -263
  190. package/src/runtime/mcp-proxy.ts +113 -113
  191. package/src/runtime/metric-parser.ts +40 -40
  192. package/src/runtime/model-fallback.ts +282 -274
  193. package/src/runtime/model-resolver.ts +118 -0
  194. package/src/runtime/output-validator.ts +187 -187
  195. package/src/runtime/overflow-recovery.ts +175 -175
  196. package/src/runtime/parallel-research.ts +44 -44
  197. package/src/runtime/parallel-utils.ts +156 -156
  198. package/src/runtime/parent-guard.ts +80 -80
  199. package/src/runtime/phase-progress.ts +217 -217
  200. package/src/runtime/pi-args.ts +165 -165
  201. package/src/runtime/pi-json-output.ts +111 -111
  202. package/src/runtime/pi-spawn.ts +167 -167
  203. package/src/runtime/policy-engine.ts +79 -79
  204. package/src/runtime/post-checks.ts +125 -125
  205. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  206. package/src/runtime/process-status.ts +97 -73
  207. package/src/runtime/progress-event-coalescer.ts +43 -43
  208. package/src/runtime/recovery-recipes.ts +74 -74
  209. package/src/runtime/retry-executor.ts +81 -81
  210. package/src/runtime/role-permission.ts +39 -39
  211. package/src/runtime/run-tracker.ts +99 -0
  212. package/src/runtime/runtime-policy.ts +21 -0
  213. package/src/runtime/runtime-resolver.ts +94 -91
  214. package/src/runtime/scheduler.ts +294 -0
  215. package/src/runtime/semaphore.ts +131 -131
  216. package/src/runtime/sensitive-paths.ts +92 -92
  217. package/src/runtime/session-usage.ts +79 -79
  218. package/src/runtime/settings-store.ts +103 -0
  219. package/src/runtime/sidechain-output.ts +29 -29
  220. package/src/runtime/skill-instructions.ts +222 -222
  221. package/src/runtime/stale-reconciler.ts +198 -189
  222. package/src/runtime/streaming-output.ts +47 -0
  223. package/src/runtime/subagent-manager.ts +404 -400
  224. package/src/runtime/subprocess-tool-registry.ts +67 -67
  225. package/src/runtime/task-display.ts +38 -38
  226. package/src/runtime/task-graph-scheduler.ts +122 -122
  227. package/src/runtime/task-graph.ts +207 -207
  228. package/src/runtime/task-output-context.ts +177 -177
  229. package/src/runtime/task-packet.ts +93 -93
  230. package/src/runtime/task-quality.ts +207 -207
  231. package/src/runtime/task-runner/capabilities.ts +78 -78
  232. package/src/runtime/task-runner/live-executor.ts +131 -113
  233. package/src/runtime/task-runner/progress.ts +119 -119
  234. package/src/runtime/task-runner/prompt-builder.ts +139 -139
  235. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  236. package/src/runtime/task-runner/result-utils.ts +14 -14
  237. package/src/runtime/task-runner/run-projection.ts +103 -103
  238. package/src/runtime/task-runner/state-helpers.ts +22 -22
  239. package/src/runtime/task-runner.ts +469 -459
  240. package/src/runtime/team-runner.ts +693 -945
  241. package/src/runtime/usage-tracker.ts +71 -0
  242. package/src/runtime/worker-heartbeat.ts +21 -21
  243. package/src/runtime/worker-startup.ts +57 -57
  244. package/src/runtime/workflow-state.ts +187 -187
  245. package/src/runtime/yield-handler.ts +190 -190
  246. package/src/schema/config-schema.ts +172 -168
  247. package/src/schema/team-tool-schema.ts +126 -126
  248. package/src/schema/validation-types.ts +151 -148
  249. package/src/skills/discover-skills.ts +67 -67
  250. package/src/skills/skill-templates.ts +374 -374
  251. package/src/state/active-run-registry.ts +227 -191
  252. package/src/state/artifact-store.ts +130 -129
  253. package/src/state/atomic-write.ts +262 -195
  254. package/src/state/blob-store.ts +116 -116
  255. package/src/state/contracts.ts +111 -111
  256. package/src/state/event-log-rotation.ts +161 -158
  257. package/src/state/event-log.ts +383 -303
  258. package/src/state/event-reconstructor.ts +217 -217
  259. package/src/state/jsonl-writer.ts +82 -82
  260. package/src/state/locks.ts +146 -146
  261. package/src/state/mailbox.ts +446 -405
  262. package/src/state/state-store.ts +364 -351
  263. package/src/state/task-claims.ts +44 -44
  264. package/src/state/types.ts +285 -285
  265. package/src/state/usage.ts +29 -29
  266. package/src/subagents/async-entry.ts +1 -1
  267. package/src/subagents/index.ts +3 -3
  268. package/src/subagents/live/control.ts +1 -1
  269. package/src/subagents/live/manager.ts +1 -1
  270. package/src/subagents/live/realtime.ts +1 -1
  271. package/src/subagents/live/session-runtime.ts +1 -1
  272. package/src/subagents/manager.ts +1 -1
  273. package/src/subagents/spawn.ts +1 -1
  274. package/src/teams/discover-teams.ts +116 -116
  275. package/src/teams/team-config.ts +27 -27
  276. package/src/teams/team-serializer.ts +38 -38
  277. package/src/types/diff.d.ts +18 -18
  278. package/src/ui/agent-management-overlay.ts +144 -144
  279. package/src/ui/crew-widget.ts +487 -370
  280. package/src/ui/dashboard-panes/agents-pane.ts +109 -28
  281. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  282. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  283. package/src/ui/dashboard-panes/health-pane.ts +30 -30
  284. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  285. package/src/ui/dashboard-panes/progress-pane.ts +30 -30
  286. package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
  287. package/src/ui/heartbeat-aggregator.ts +63 -63
  288. package/src/ui/keybinding-map.ts +97 -94
  289. package/src/ui/live-conversation-overlay.ts +152 -0
  290. package/src/ui/live-run-sidebar.ts +180 -180
  291. package/src/ui/mascot.ts +442 -442
  292. package/src/ui/overlays/agent-picker-overlay.ts +57 -57
  293. package/src/ui/overlays/confirm-overlay.ts +58 -58
  294. package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
  295. package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
  296. package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
  297. package/src/ui/pi-ui-compat.ts +57 -57
  298. package/src/ui/powerbar-publisher.ts +221 -197
  299. package/src/ui/render-scheduler.ts +216 -143
  300. package/src/ui/run-action-dispatcher.ts +118 -118
  301. package/src/ui/run-dashboard.ts +526 -464
  302. package/src/ui/run-event-bus.ts +208 -208
  303. package/src/ui/run-snapshot-cache.ts +826 -777
  304. package/src/ui/settings-overlay.ts +721 -0
  305. package/src/ui/snapshot-types.ts +86 -70
  306. package/src/ui/theme-adapter.ts +190 -190
  307. package/src/ui/tool-progress-formatter.ts +89 -0
  308. package/src/ui/transcript-cache.ts +94 -94
  309. package/src/ui/transcript-viewer.ts +335 -335
  310. package/src/utils/conflict-detect.ts +662 -0
  311. package/src/utils/file-coalescer.ts +86 -86
  312. package/src/utils/frontmatter.ts +68 -68
  313. package/src/utils/fs-watch.ts +88 -31
  314. package/src/utils/gh-protocol.ts +479 -0
  315. package/src/utils/ids.ts +17 -17
  316. package/src/utils/incremental-reader.ts +104 -104
  317. package/src/utils/internal-error.ts +6 -6
  318. package/src/utils/names.ts +27 -27
  319. package/src/utils/paths.ts +102 -63
  320. package/src/utils/redaction.ts +44 -44
  321. package/src/utils/safe-paths.ts +47 -47
  322. package/src/utils/scan-cache.ts +136 -136
  323. package/src/utils/sse-parser.ts +134 -134
  324. package/src/utils/task-name-generator.ts +337 -337
  325. package/src/utils/timings.ts +33 -33
  326. package/src/utils/visual.ts +243 -198
  327. package/src/workflows/discover-workflows.ts +139 -139
  328. package/src/workflows/validate-workflow.ts +40 -40
  329. package/src/workflows/workflow-config.ts +26 -26
  330. package/src/workflows/workflow-serializer.ts +32 -32
  331. package/src/worktree/branch-freshness.ts +45 -45
  332. package/src/worktree/cleanup.ts +75 -75
  333. package/src/worktree/worktree-manager.ts +188 -188
  334. package/teams/default.team.md +12 -12
  335. package/teams/fast-fix.team.md +11 -11
  336. package/teams/implementation.team.md +18 -18
  337. package/teams/parallel-research.team.md +14 -14
  338. package/teams/research.team.md +11 -11
  339. package/teams/review.team.md +12 -12
  340. package/tsconfig.json +19 -19
  341. package/workflows/default.workflow.md +30 -30
  342. package/workflows/fast-fix.workflow.md +23 -23
  343. package/workflows/implementation.workflow.md +43 -43
  344. package/workflows/parallel-research.workflow.md +46 -46
  345. package/workflows/research.workflow.md +22 -22
  346. package/workflows/review.workflow.md +30 -30
  347. package/skills/task-packet/SKILL.md +0 -28
  348. package/skills/verify-evidence/SKILL.md +0 -27
@@ -1,179 +1,377 @@
1
- import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
2
- import type { IrcMessage } from "./live-irc.ts";
3
-
4
- type LiveSessionHandle = {
5
- steer?: (text: string) => Promise<void>;
6
- prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>;
7
- abort?: () => Promise<void> | void;
8
- };
9
-
10
- export interface LiveAgentHandle {
11
- agentId: string;
12
- taskId: string;
13
- runId: string;
14
- session: LiveSessionHandle;
15
- createdAt: string;
16
- updatedAt: string;
17
- status: CrewAgentRecord["status"];
18
- pendingSteers: string[];
19
- pendingFollowUps: string[];
20
- /** Phase 7: Pending IRC messages for this agent. */
21
- pendingMessages: IrcMessage[];
22
- }
23
-
24
- const liveAgents = new Map<string, LiveAgentHandle>();
25
-
26
- export function registerLiveAgent(input: Omit<LiveAgentHandle, "createdAt" | "updatedAt" | "pendingSteers" | "pendingFollowUps" | "pendingMessages">): LiveAgentHandle {
27
- const now = new Date().toISOString();
28
- const existing = liveAgents.get(input.agentId);
29
- const handle: LiveAgentHandle = { ...input, createdAt: existing?.createdAt ?? now, updatedAt: now, pendingSteers: existing?.pendingSteers ?? [], pendingFollowUps: existing?.pendingFollowUps ?? [], pendingMessages: existing?.pendingMessages ?? [] };
30
- liveAgents.set(input.agentId, handle);
31
- if (handle.pendingSteers.length && typeof handle.session.steer === "function") {
32
- const pending = [...handle.pendingSteers];
33
- handle.pendingSteers.length = 0;
34
- for (const message of pending) void handle.session.steer(message).catch(() => {});
35
- }
36
- if (handle.pendingFollowUps.length && typeof handle.session.prompt === "function") {
37
- const pending = [...handle.pendingFollowUps];
38
- handle.pendingFollowUps.length = 0;
39
- for (const message of pending) void handle.session.prompt(message, { source: "api", expandPromptTemplates: false }).catch(() => {});
40
- }
41
- return handle;
42
- }
43
-
44
- export function updateLiveAgentStatus(agentId: string, status: CrewAgentRecord["status"]): void {
45
- const handle = liveAgents.get(agentId);
46
- if (!handle) return;
47
- handle.status = status;
48
- handle.updatedAt = new Date().toISOString();
49
- }
50
-
51
- export function getLiveAgent(agentIdOrTaskId: string): LiveAgentHandle | undefined {
52
- return liveAgents.get(agentIdOrTaskId) ?? [...liveAgents.values()].find((entry) => entry.taskId === agentIdOrTaskId);
53
- }
54
-
55
- export function listLiveAgents(): LiveAgentHandle[] {
56
- return [...liveAgents.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
57
- }
58
-
59
- export async function steerLiveAgent(agentIdOrTaskId: string, message: string): Promise<LiveAgentHandle> {
60
- const handle = getLiveAgent(agentIdOrTaskId);
61
- if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
62
- if (typeof handle.session.steer !== "function") {
63
- handle.pendingSteers.push(message);
64
- return handle;
65
- }
66
- await handle.session.steer(message);
67
- handle.updatedAt = new Date().toISOString();
68
- return handle;
69
- }
70
-
71
- export async function followUpLiveAgent(agentIdOrTaskId: string, prompt: string): Promise<LiveAgentHandle> {
72
- const handle = getLiveAgent(agentIdOrTaskId);
73
- if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
74
- if (typeof handle.session.prompt !== "function") {
75
- handle.pendingFollowUps.push(prompt);
76
- return handle;
77
- }
78
- await handle.session.prompt(prompt, { source: "api", expandPromptTemplates: false });
79
- handle.updatedAt = new Date().toISOString();
80
- return handle;
81
- }
82
-
83
- export async function stopLiveAgent(agentIdOrTaskId: string): Promise<LiveAgentHandle> {
84
- const handle = getLiveAgent(agentIdOrTaskId);
85
- if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
86
- if (typeof handle.session.abort !== "function") throw new Error(`Live agent '${agentIdOrTaskId}' does not expose abort().`);
87
- await handle.session.abort();
88
- handle.status = "stopped";
89
- handle.updatedAt = new Date().toISOString();
90
- return handle;
91
- }
92
-
93
- export async function resumeLiveAgent(agentIdOrTaskId: string, prompt: string): Promise<LiveAgentHandle> {
94
- const handle = getLiveAgent(agentIdOrTaskId);
95
- if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
96
- if (typeof handle.session.prompt !== "function") throw new Error(`Live agent '${agentIdOrTaskId}' does not expose prompt().`);
97
- handle.status = "running";
98
- await handle.session.prompt(prompt, { source: "api", expandPromptTemplates: false });
99
- handle.status = "completed";
100
- handle.updatedAt = new Date().toISOString();
101
- return handle;
102
- }
103
-
104
- export function clearLiveAgentsForTest(): void {
105
- liveAgents.clear();
106
- }
107
-
108
- /** Phase 7/G4: Send an IRC message to a specific live agent (DM).
109
- * Uses non-blocking delivery via sendCustomMessage when available.
110
- * Falls back to session.prompt (blocking) when not.
111
- */
112
- export function sendIrcMessage(targetAgentId: string, message: IrcMessage): void {
113
- const handle = getLiveAgent(targetAgentId);
114
- if (!handle) return;
115
- handle.pendingMessages.push(message);
116
- handle.updatedAt = new Date().toISOString();
117
- // G4: Try non-blocking delivery via sendCustomMessage
118
- const session = handle.session as Record<string, unknown>;
119
- if (typeof session.sendCustomMessage === "function") {
120
- try {
121
- (session.sendCustomMessage as (msg: unknown, opts?: unknown) => void)(
122
- { customType: "irc", content: `[DM from ${message.from}] ${message.content}`, display: "collapsed" },
123
- { deliverAs: "followUp", triggerTurn: false },
124
- );
125
- return;
126
- } catch {
127
- // Fall through to prompt-based delivery
128
- }
129
- }
130
- // Fallback: inject as prompt (blocking)
131
- if (typeof handle.session.prompt === "function") {
132
- const ircPrompt = `[Message from ${message.from}] ${message.content}`;
133
- void handle.session.prompt(ircPrompt, { source: "api", expandPromptTemplates: false }).catch(() => {});
134
- }
135
- }
136
-
137
- /** Phase 7/G4: Broadcast an IRC message to all live agents except the sender.
138
- * Uses non-blocking delivery via sendCustomMessage when available.
139
- * Returns recipient IDs.
140
- */
141
- export function broadcastIrcMessage(fromAgentId: string, message: IrcMessage): string[] {
142
- const recipients: string[] = [];
143
- for (const handle of liveAgents.values()) {
144
- if (handle.agentId === fromAgentId) continue;
145
- if (handle.status !== "running" && handle.status !== "queued") continue;
146
- handle.pendingMessages.push(message);
147
- handle.updatedAt = new Date().toISOString();
148
- // G4: Try non-blocking delivery
149
- const session = handle.session as Record<string, unknown>;
150
- if (typeof session.sendCustomMessage === "function") {
151
- try {
152
- (session.sendCustomMessage as (msg: unknown, opts?: unknown) => void)(
153
- { customType: "irc", content: `[Broadcast from ${message.from}] ${message.content}`, display: "collapsed" },
154
- { deliverAs: "followUp", triggerTurn: false },
155
- );
156
- recipients.push(handle.agentId);
157
- continue;
158
- } catch {
159
- // Fall through to prompt-based delivery
160
- }
161
- }
162
- // Fallback: inject as prompt
163
- if (typeof handle.session.prompt === "function") {
164
- const ircPrompt = `[Broadcast from ${message.from}] ${message.content}`;
165
- void handle.session.prompt(ircPrompt, { source: "api", expandPromptTemplates: false }).catch(() => {});
166
- }
167
- recipients.push(handle.agentId);
168
- }
169
- return recipients;
170
- }
171
-
172
- /** Phase 7: Get pending IRC messages for an agent (and clear them). */
173
- export function drainIrcMessages(agentIdOrTaskId: string): IrcMessage[] {
174
- const handle = getLiveAgent(agentIdOrTaskId);
175
- if (!handle) return [];
176
- const messages = [...handle.pendingMessages];
177
- handle.pendingMessages.length = 0;
178
- return messages;
179
- }
1
+ import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
2
+ import type { IrcMessage } from "./live-irc.ts";
3
+ import { logInternalError } from "../utils/internal-error.ts";
4
+ import type { appendEvent } from "../state/event-log.ts";
5
+
6
+ type LiveSessionHandle = {
7
+ steer?: (text: string) => Promise<void>;
8
+ prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>;
9
+ abort?: () => Promise<void> | void;
10
+ dispose?: () => void;
11
+ /** Upstream session stats (input/output/cacheWrite tokens, context %). */
12
+ getSessionStats?: () => {
13
+ tokens?: { input?: number; output?: number; cacheWrite?: number; cacheRead?: number };
14
+ contextUsage?: { percent?: number | null; window?: number };
15
+ };
16
+ };
17
+
18
+ /** Real-time activity state for a live-session agent. */
19
+ export interface LiveAgentActivity {
20
+ /** Currently active tools (toolName description). */
21
+ activeTools: Map<string, string>;
22
+ /** Total tool invocations. */
23
+ toolUses: number;
24
+ /** Current turn count. */
25
+ turnCount: number;
26
+ /** Effective max turns (undefined = unlimited). */
27
+ maxTurns?: number;
28
+ /** Latest assistant text snippet. */
29
+ responseText: string;
30
+ /** Number of context compactions survived. */
31
+ compactionCount: number;
32
+ /** Started-at timestamp (ms epoch). */
33
+ startedAtMs: number;
34
+ /** Completed-at timestamp (ms epoch, 0 = still running). */
35
+ completedAtMs: number;
36
+ /** Model name used for this agent (e.g. "sonnet", "haiku"). */
37
+ modelName?: string;
38
+ }
39
+
40
+ export interface LiveAgentHandle {
41
+ agentId: string;
42
+ taskId: string;
43
+ runId: string;
44
+ /** Workspace where this agent was spawned — used for session-scoped visibility. */
45
+ workspaceId: string;
46
+ role?: string;
47
+ agent?: string;
48
+ description?: string;
49
+ /** Model name used for this agent (e.g. "sonnet", "haiku"). */
50
+ modelName?: string;
51
+ session: LiveSessionHandle;
52
+ createdAt: string;
53
+ updatedAt: string;
54
+ status: CrewAgentRecord["status"];
55
+ pendingSteers: string[];
56
+ pendingFollowUps: string[];
57
+ /** Phase 7: Pending IRC messages for this agent. */
58
+ pendingMessages: IrcMessage[];
59
+ /** G1-G6: Real-time activity tracking (in-memory only). */
60
+ activity: LiveAgentActivity;
61
+ }
62
+
63
+ const liveAgents = new Map<string, LiveAgentHandle>();
64
+
65
+ /**
66
+ * List all live agents for a specific workspace.
67
+ * Only agents belonging to the given workspaceId are returned.
68
+ */
69
+ export function listLiveAgentsByWorkspace(workspaceId: string): LiveAgentHandle[] {
70
+ return listLiveAgents().filter((a) => a.workspaceId === workspaceId);
71
+ }
72
+
73
+ /**
74
+ * List only active agents (running/queued/waiting) for a specific workspace.
75
+ */
76
+ export function listActiveLiveAgentsByWorkspace(workspaceId: string): LiveAgentHandle[] {
77
+ return listActiveLiveAgents().filter((a) => a.workspaceId === workspaceId);
78
+ }
79
+
80
+ export function registerLiveAgent(input: Omit<LiveAgentHandle, "createdAt" | "updatedAt" | "pendingSteers" | "pendingFollowUps" | "pendingMessages" | "activity"> & { workspaceId: string }, eventLogFn?: typeof appendEvent, eventsPath?: string): LiveAgentHandle {
81
+ const now = new Date().toISOString();
82
+ const existing = liveAgents.get(input.agentId);
83
+ const handle: LiveAgentHandle = {
84
+ ...input,
85
+ createdAt: existing?.createdAt ?? now,
86
+ updatedAt: now,
87
+ pendingSteers: existing?.pendingSteers ?? [],
88
+ pendingFollowUps: existing?.pendingFollowUps ?? [],
89
+ pendingMessages: existing?.pendingMessages ?? [],
90
+ activity: existing?.activity ?? {
91
+ activeTools: new Map(),
92
+ toolUses: 0,
93
+ turnCount: 0,
94
+ responseText: "",
95
+ compactionCount: 0,
96
+ startedAtMs: Date.now(),
97
+ completedAtMs: 0,
98
+ modelName: undefined,
99
+ },
100
+ };
101
+ liveAgents.set(input.agentId, handle);
102
+ try { if (eventLogFn && eventsPath) eventLogFn(eventsPath, { type: "live_agent.registered", runId: input.runId, taskId: input.taskId, message: `Live agent registered: ${input.agent} (${input.role})`, data: { agentId: input.agentId, role: input.role, agent: input.agent, workspaceId: input.workspaceId } }); } catch { /* non-critical */ }
103
+ if (handle.pendingSteers.length && typeof handle.session.steer === "function") {
104
+ const pending = [...handle.pendingSteers];
105
+ handle.pendingSteers.length = 0;
106
+ for (const message of pending) void handle.session.steer(message).catch(() => {});
107
+ }
108
+ if (handle.pendingFollowUps.length && typeof handle.session.prompt === "function") {
109
+ const pending = [...handle.pendingFollowUps];
110
+ handle.pendingFollowUps.length = 0;
111
+ for (const message of pending) void handle.session.prompt(message, { source: "api", expandPromptTemplates: false }).catch(() => {});
112
+ }
113
+ return handle;
114
+ }
115
+
116
+ export function updateLiveAgentStatus(agentId: string, status: CrewAgentRecord["status"]): void {
117
+ const handle = liveAgents.get(agentId);
118
+ if (!handle) return;
119
+ handle.status = status;
120
+ handle.updatedAt = new Date().toISOString();
121
+ }
122
+
123
+ function safeDisposeLiveSession(handle: LiveAgentHandle): void {
124
+ try { handle.session.dispose?.(); } catch (error) {
125
+ logInternalError("live-agent-manager.dispose", error, `agentId=${handle.agentId}`);
126
+ }
127
+ }
128
+
129
+ export function removeLiveAgentHandle(agentId: string): LiveAgentHandle | undefined {
130
+ const handle = liveAgents.get(agentId);
131
+ if (!handle) return undefined;
132
+ liveAgents.delete(agentId);
133
+ safeDisposeLiveSession(handle);
134
+ return handle;
135
+ }
136
+
137
+ export function disposeLiveAgentSession(agentIdOrTaskId: string): void {
138
+ const handle = getLiveAgent(agentIdOrTaskId);
139
+ if (!handle) return;
140
+ safeDisposeLiveSession(handle);
141
+ }
142
+
143
+ export async function terminateLiveAgent(agentIdOrTaskId: string, status: CrewAgentRecord["status"] = "stopped", eventLogFn?: typeof appendEvent, eventsPath?: string): Promise<LiveAgentHandle | undefined> {
144
+ const handle = getLiveAgent(agentIdOrTaskId);
145
+ if (!handle) return undefined;
146
+ handle.status = status;
147
+ handle.updatedAt = new Date().toISOString();
148
+ liveAgents.delete(handle.agentId);
149
+ try { if (eventLogFn && eventsPath) eventLogFn(eventsPath, { type: "live_agent.terminated", runId: handle.runId, taskId: handle.taskId, message: `Live agent terminated: ${handle.agent} status=${status}`, data: { agentId: handle.agentId, status, role: handle.role, workspaceId: handle.workspaceId } }); } catch { /* non-critical */ }
150
+ try {
151
+ await handle.session.abort?.();
152
+ } finally {
153
+ safeDisposeLiveSession(handle);
154
+ }
155
+ return handle;
156
+ }
157
+
158
+ export async function terminateLiveAgentsForRun(runId: string, status: CrewAgentRecord["status"] = "failed", eventLogFn?: typeof appendEvent, eventsPath?: string): Promise<number> {
159
+ const agents = [...liveAgents.values()].filter((agent) => agent.runId === runId);
160
+ await Promise.all(agents.map((agent) => terminateLiveAgent(agent.agentId, status, eventLogFn, eventsPath)));
161
+ return agents.length;
162
+ }
163
+
164
+ export function getLiveAgent(agentIdOrTaskId: string): LiveAgentHandle | undefined {
165
+ return liveAgents.get(agentIdOrTaskId) ?? [...liveAgents.values()].find((entry) => entry.taskId === agentIdOrTaskId);
166
+ }
167
+
168
+ /** Maximum time a terminal live agent handle stays in memory (10 minutes). */
169
+ const STALE_HANDLE_MS = 10 * 60 * 1000;
170
+ /** Maximum time a running/queued live agent handle stays without any update (30 minutes).
171
+ * After this, the agent is presumed dead — the real process would have updated the handle. */
172
+ const STALE_RUNNING_HANDLE_MS = 30 * 60 * 1000;
173
+
174
+ /** Remove dead live agent handles.
175
+ * Evicts: (1) terminal-status handles older than STALE_HANDLE_MS, and
176
+ * (2) running/queued handles with no update for STALE_RUNNING_HANDLE_MS.
177
+ * Called periodically by the widget refresh cycle.
178
+ * Returns the number of handles evicted.
179
+ */
180
+ export function evictStaleLiveAgentHandles(now = Date.now()): number {
181
+ let evicted = 0;
182
+ for (const [agentId, handle] of liveAgents) {
183
+ const age = now - new Date(handle.updatedAt).getTime();
184
+ const isActive = handle.status === "running" || handle.status === "queued" || handle.status === "waiting";
185
+ if (!isActive) {
186
+ // Terminal handle — evict after grace period
187
+ if (age > STALE_HANDLE_MS) {
188
+ liveAgents.delete(agentId);
189
+ safeDisposeLiveSession(handle);
190
+ evicted++;
191
+ }
192
+ } else if (age > STALE_RUNNING_HANDLE_MS) {
193
+ // Active-status handle with no update for 30min — presumed dead
194
+ liveAgents.delete(agentId);
195
+ safeDisposeLiveSession(handle);
196
+ evicted++;
197
+ }
198
+ }
199
+ return evicted;
200
+ }
201
+
202
+ export function listLiveAgents(): LiveAgentHandle[] {
203
+ return [...liveAgents.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
204
+ }
205
+
206
+ export function listActiveLiveAgents(): LiveAgentHandle[] {
207
+ return listLiveAgents().filter((agent) => agent.status === "running" || agent.status === "queued" || agent.status === "waiting");
208
+ }
209
+
210
+ export function getLiveAgentContextPercent(agentIdOrTaskId: string): number | null {
211
+ const handle = getLiveAgent(agentIdOrTaskId);
212
+ if (!handle || handle.status !== "running") return null;
213
+ try {
214
+ return handle.session.getSessionStats?.().contextUsage?.percent ?? null;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ export async function steerLiveAgent(agentIdOrTaskId: string, message: string): Promise<LiveAgentHandle> {
221
+ const handle = getLiveAgent(agentIdOrTaskId);
222
+ if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
223
+ if (typeof handle.session.steer !== "function") {
224
+ handle.pendingSteers.push(message);
225
+ return handle;
226
+ }
227
+ await handle.session.steer(message);
228
+ handle.updatedAt = new Date().toISOString();
229
+ return handle;
230
+ }
231
+
232
+ export async function followUpLiveAgent(agentIdOrTaskId: string, prompt: string): Promise<LiveAgentHandle> {
233
+ const handle = getLiveAgent(agentIdOrTaskId);
234
+ if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
235
+ if (typeof handle.session.prompt !== "function") {
236
+ handle.pendingFollowUps.push(prompt);
237
+ return handle;
238
+ }
239
+ await handle.session.prompt(prompt, { source: "api", expandPromptTemplates: false });
240
+ handle.updatedAt = new Date().toISOString();
241
+ return handle;
242
+ }
243
+
244
+ export async function stopLiveAgent(agentIdOrTaskId: string): Promise<LiveAgentHandle> {
245
+ const stopped = await terminateLiveAgent(agentIdOrTaskId, "stopped");
246
+ if (!stopped) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
247
+ return stopped;
248
+ }
249
+
250
+ export async function resumeLiveAgent(agentIdOrTaskId: string, prompt: string): Promise<LiveAgentHandle> {
251
+ const handle = getLiveAgent(agentIdOrTaskId);
252
+ if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
253
+ if (typeof handle.session.prompt !== "function") throw new Error(`Live agent '${agentIdOrTaskId}' does not expose prompt().`);
254
+ handle.status = "running";
255
+ await handle.session.prompt(prompt, { source: "api", expandPromptTemplates: false });
256
+ handle.status = "completed";
257
+ handle.updatedAt = new Date().toISOString();
258
+ return handle;
259
+ }
260
+
261
+ /** G2: Track tool start for a live agent. */
262
+ export function trackLiveAgentToolStart(agentIdOrTaskId: string, toolName: string): void {
263
+ const handle = getLiveAgent(agentIdOrTaskId);
264
+ if (!handle) return;
265
+ handle.activity.activeTools.set(toolName, toolName);
266
+ handle.activity.toolUses++;
267
+ handle.updatedAt = new Date().toISOString();
268
+ }
269
+
270
+ /** G2: Track tool end for a live agent. */
271
+ export function trackLiveAgentToolEnd(agentIdOrTaskId: string, toolName: string): void {
272
+ const handle = getLiveAgent(agentIdOrTaskId);
273
+ if (!handle) return;
274
+ handle.activity.activeTools.delete(toolName);
275
+ }
276
+
277
+ /** G3/G6: Track turn end and compaction. */
278
+ export function trackLiveAgentTurnEnd(agentIdOrTaskId: string, compaction = false): void {
279
+ const handle = getLiveAgent(agentIdOrTaskId);
280
+ if (!handle) return;
281
+ handle.activity.turnCount++;
282
+ if (compaction) handle.activity.compactionCount++;
283
+ handle.activity.activeTools.clear();
284
+ handle.updatedAt = new Date().toISOString();
285
+ }
286
+
287
+ /** G2: Track assistant response text. */
288
+ export function trackLiveAgentResponseText(agentIdOrTaskId: string, text: string): void {
289
+ const handle = getLiveAgent(agentIdOrTaskId);
290
+ if (!handle) return;
291
+ handle.activity.responseText = text.slice(-200);
292
+ }
293
+
294
+ /** Mark live agent completed with timestamp. */
295
+ export function markLiveAgentCompleted(agentIdOrTaskId: string): void {
296
+ const handle = getLiveAgent(agentIdOrTaskId);
297
+ if (!handle) return;
298
+ handle.activity.completedAtMs = Date.now();
299
+ handle.activity.activeTools.clear();
300
+ }
301
+
302
+ export function clearLiveAgentsForTest(): void {
303
+ liveAgents.clear();
304
+ }
305
+
306
+ /** Phase 7/G4: Send an IRC message to a specific live agent (DM).
307
+ * Uses non-blocking delivery via sendCustomMessage when available.
308
+ * Falls back to session.prompt (blocking) when not.
309
+ */
310
+ export function sendIrcMessage(targetAgentId: string, message: IrcMessage): void {
311
+ const handle = getLiveAgent(targetAgentId);
312
+ if (!handle) return;
313
+ handle.pendingMessages.push(message);
314
+ handle.updatedAt = new Date().toISOString();
315
+ // G4: Try non-blocking delivery via sendCustomMessage
316
+ const session = handle.session as Record<string, unknown>;
317
+ if (typeof session.sendCustomMessage === "function") {
318
+ try {
319
+ (session.sendCustomMessage as (msg: unknown, opts?: unknown) => void)(
320
+ { customType: "irc", content: `[DM from ${message.from}] ${message.content}`, display: "collapsed" },
321
+ { deliverAs: "followUp", triggerTurn: false },
322
+ );
323
+ return;
324
+ } catch {
325
+ // Fall through to prompt-based delivery
326
+ }
327
+ }
328
+ // Fallback: inject as prompt (blocking)
329
+ if (typeof handle.session.prompt === "function") {
330
+ const ircPrompt = `[Message from ${message.from}] ${message.content}`;
331
+ void handle.session.prompt(ircPrompt, { source: "api", expandPromptTemplates: false }).catch(() => {});
332
+ }
333
+ }
334
+
335
+ /** Phase 7/G4: Broadcast an IRC message to all live agents except the sender.
336
+ * Uses non-blocking delivery via sendCustomMessage when available.
337
+ * Returns recipient IDs.
338
+ */
339
+ export function broadcastIrcMessage(fromAgentId: string, message: IrcMessage): string[] {
340
+ const recipients: string[] = [];
341
+ for (const handle of liveAgents.values()) {
342
+ if (handle.agentId === fromAgentId) continue;
343
+ if (handle.status !== "running" && handle.status !== "queued") continue;
344
+ handle.pendingMessages.push(message);
345
+ handle.updatedAt = new Date().toISOString();
346
+ // G4: Try non-blocking delivery
347
+ const session = handle.session as Record<string, unknown>;
348
+ if (typeof session.sendCustomMessage === "function") {
349
+ try {
350
+ (session.sendCustomMessage as (msg: unknown, opts?: unknown) => void)(
351
+ { customType: "irc", content: `[Broadcast from ${message.from}] ${message.content}`, display: "collapsed" },
352
+ { deliverAs: "followUp", triggerTurn: false },
353
+ );
354
+ recipients.push(handle.agentId);
355
+ continue;
356
+ } catch {
357
+ // Fall through to prompt-based delivery
358
+ }
359
+ }
360
+ // Fallback: inject as prompt
361
+ if (typeof handle.session.prompt === "function") {
362
+ const ircPrompt = `[Broadcast from ${message.from}] ${message.content}`;
363
+ void handle.session.prompt(ircPrompt, { source: "api", expandPromptTemplates: false }).catch(() => {});
364
+ }
365
+ recipients.push(handle.agentId);
366
+ }
367
+ return recipients;
368
+ }
369
+
370
+ /** Phase 7: Get pending IRC messages for an agent (and clear them). */
371
+ export function drainIrcMessages(agentIdOrTaskId: string): IrcMessage[] {
372
+ const handle = getLiveAgent(agentIdOrTaskId);
373
+ if (!handle) return [];
374
+ const messages = [...handle.pendingMessages];
375
+ handle.pendingMessages.length = 0;
376
+ return messages;
377
+ }
@@ -1,36 +1,36 @@
1
- import type { LiveAgentControlRequest } from "./live-agent-control.ts";
2
-
3
- export interface LiveControlRealtimeMessage {
4
- type: "live-control";
5
- version: 1;
6
- request: LiveAgentControlRequest;
7
- }
8
-
9
- type Listener = (request: LiveAgentControlRequest) => void | Promise<void>;
10
-
11
- const listeners = new Set<Listener>();
12
-
13
- export function publishLiveControlRealtime(request: LiveAgentControlRequest): void {
14
- for (const listener of [...listeners]) void listener(request);
15
- }
16
-
17
- export function subscribeLiveControlRealtime(listener: Listener): () => void {
18
- listeners.add(listener);
19
- return () => listeners.delete(listener);
20
- }
21
-
22
- export function liveControlRealtimeMessage(request: LiveAgentControlRequest): LiveControlRealtimeMessage {
23
- return { type: "live-control", version: 1, request };
24
- }
25
-
26
- export function parseLiveControlRealtimeMessage(raw: unknown): LiveAgentControlRequest | undefined {
27
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
28
- const message = raw as { type?: unknown; version?: unknown; request?: unknown };
29
- if (message.type !== "live-control" || message.version !== 1 || !message.request || typeof message.request !== "object" || Array.isArray(message.request)) return undefined;
30
- const request = message.request as Partial<LiveAgentControlRequest>;
31
- return typeof request.id === "string" && typeof request.runId === "string" && typeof request.taskId === "string" && (request.operation === "steer" || request.operation === "follow-up" || request.operation === "stop" || request.operation === "resume") && typeof request.createdAt === "string" ? request as LiveAgentControlRequest : undefined;
32
- }
33
-
34
- export function clearLiveControlRealtimeForTest(): void {
35
- listeners.clear();
36
- }
1
+ import type { LiveAgentControlRequest } from "./live-agent-control.ts";
2
+
3
+ export interface LiveControlRealtimeMessage {
4
+ type: "live-control";
5
+ version: 1;
6
+ request: LiveAgentControlRequest;
7
+ }
8
+
9
+ type Listener = (request: LiveAgentControlRequest) => void | Promise<void>;
10
+
11
+ const listeners = new Set<Listener>();
12
+
13
+ export function publishLiveControlRealtime(request: LiveAgentControlRequest): void {
14
+ for (const listener of [...listeners]) void listener(request);
15
+ }
16
+
17
+ export function subscribeLiveControlRealtime(listener: Listener): () => void {
18
+ listeners.add(listener);
19
+ return () => listeners.delete(listener);
20
+ }
21
+
22
+ export function liveControlRealtimeMessage(request: LiveAgentControlRequest): LiveControlRealtimeMessage {
23
+ return { type: "live-control", version: 1, request };
24
+ }
25
+
26
+ export function parseLiveControlRealtimeMessage(raw: unknown): LiveAgentControlRequest | undefined {
27
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
28
+ const message = raw as { type?: unknown; version?: unknown; request?: unknown };
29
+ if (message.type !== "live-control" || message.version !== 1 || !message.request || typeof message.request !== "object" || Array.isArray(message.request)) return undefined;
30
+ const request = message.request as Partial<LiveAgentControlRequest>;
31
+ return typeof request.id === "string" && typeof request.runId === "string" && typeof request.taskId === "string" && (request.operation === "steer" || request.operation === "follow-up" || request.operation === "stop" || request.operation === "resume") && typeof request.createdAt === "string" ? request as LiveAgentControlRequest : undefined;
32
+ }
33
+
34
+ export function clearLiveControlRealtimeForTest(): void {
35
+ listeners.clear();
36
+ }