pi-crew 0.2.2 → 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 (354) hide show
  1. package/AGENTS.md +57 -32
  2. package/CHANGELOG.md +466 -413
  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 -0
  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-plan-2026-05-12.md +463 -0
  25. package/docs/followup-review-2026-05-12.md +297 -0
  26. package/docs/followup-review-round3-2026-05-12.md +342 -0
  27. package/docs/followup-review-round4-2026-05-13.md +107 -0
  28. package/docs/implementation-plan-top3.md +333 -0
  29. package/docs/live-mailbox-runtime.md +36 -36
  30. package/docs/next-upgrade-roadmap.md +808 -808
  31. package/docs/oh-my-pi-research.md +509 -0
  32. package/docs/perf/baseline-2026-05.md +113 -0
  33. package/docs/perf/final-report-2026-05.md +206 -0
  34. package/docs/perf/sprint-1-report.md +71 -0
  35. package/docs/perf/sprint-2-report.md +81 -0
  36. package/docs/perf/sprint-2.5-report.md +53 -0
  37. package/docs/perf/sprint-3-report.md +36 -0
  38. package/docs/perf/sprint-4-report.md +47 -0
  39. package/docs/perf/sprint-5-report.md +51 -0
  40. package/docs/perf/sprint-6-report.md +94 -0
  41. package/docs/perf/sprint-7-report.md +74 -0
  42. package/docs/perf/upgrade-plan-2026-05.md +147 -0
  43. package/docs/pi-subagents3-deep-analysis.md +508 -0
  44. package/docs/product/README.md +31 -0
  45. package/docs/product/platform.md +27 -0
  46. package/docs/product/runtime-safety.md +37 -0
  47. package/docs/product/team-run.md +39 -0
  48. package/docs/product/team-tool.md +37 -0
  49. package/docs/publishing.md +65 -65
  50. package/docs/resource-formats.md +134 -134
  51. package/docs/runtime-analysis-child-vs-live.md +171 -0
  52. package/docs/runtime-flow.md +148 -148
  53. package/docs/runtime-migration-in-process-analysis.md +250 -0
  54. package/docs/stories/README.md +30 -0
  55. package/docs/stories/backlog.md +36 -0
  56. package/docs/templates/decision.md +27 -0
  57. package/docs/templates/story.md +44 -0
  58. package/docs/templates/validation-report.md +32 -0
  59. package/docs/usage.md +238 -238
  60. package/index.ts +7 -6
  61. package/install.mjs +65 -65
  62. package/package.json +107 -99
  63. package/schema.json +222 -222
  64. package/skills/child-pi-spawning/SKILL.md +213 -0
  65. package/skills/context-artifact-hygiene/SKILL.md +32 -0
  66. package/skills/event-log-tracing/SKILL.md +299 -0
  67. package/skills/git-master/SKILL.md +225 -24
  68. package/skills/live-agent-lifecycle/SKILL.md +192 -0
  69. package/skills/mailbox-interactive/SKILL.md +300 -19
  70. package/skills/model-routing-context/SKILL.md +94 -0
  71. package/skills/multi-perspective-review/SKILL.md +88 -0
  72. package/skills/read-only-explorer/SKILL.md +250 -26
  73. package/skills/safe-bash/SKILL.md +307 -21
  74. package/skills/verification-before-done/SKILL.md +11 -2
  75. package/skills/widget-rendering/SKILL.md +258 -0
  76. package/skills/workspace-isolation/SKILL.md +202 -0
  77. package/skills/worktree-isolation/SKILL.md +202 -18
  78. package/src/adapters/claude-adapter.ts +25 -25
  79. package/src/adapters/codex-adapter.ts +21 -21
  80. package/src/adapters/cursor-adapter.ts +17 -17
  81. package/src/adapters/export-util.ts +137 -137
  82. package/src/adapters/index.ts +15 -15
  83. package/src/adapters/registry.ts +18 -18
  84. package/src/adapters/types.ts +23 -23
  85. package/src/agents/agent-config.ts +38 -38
  86. package/src/agents/agent-serializer.ts +38 -38
  87. package/src/agents/discover-agents.ts +121 -118
  88. package/src/config/config.ts +740 -858
  89. package/src/config/defaults.ts +96 -96
  90. package/src/config/drift-detector.ts +211 -211
  91. package/src/config/markers.ts +327 -327
  92. package/src/config/resilient-parser.ts +109 -108
  93. package/src/config/suggestions.ts +74 -74
  94. package/src/config/types.ts +199 -0
  95. package/src/extension/async-notifier.ts +123 -89
  96. package/src/extension/autonomous-policy.ts +169 -169
  97. package/src/extension/cross-extension-rpc.ts +104 -103
  98. package/src/extension/help.ts +47 -47
  99. package/src/extension/import-index.ts +69 -69
  100. package/src/extension/management.ts +395 -382
  101. package/src/extension/notification-router.ts +116 -116
  102. package/src/extension/notification-sink.ts +51 -51
  103. package/src/extension/project-init.ts +168 -168
  104. package/src/extension/register.ts +859 -668
  105. package/src/extension/registration/artifact-cleanup.ts +15 -15
  106. package/src/extension/registration/command-utils.ts +54 -54
  107. package/src/extension/registration/commands.ts +559 -452
  108. package/src/extension/registration/compaction-guard.ts +125 -125
  109. package/src/extension/registration/subagent-helpers.ts +102 -102
  110. package/src/extension/registration/subagent-tools.ts +220 -158
  111. package/src/extension/registration/team-tool.ts +159 -98
  112. package/src/extension/registration/viewers.ts +29 -0
  113. package/src/extension/result-watcher.ts +128 -128
  114. package/src/extension/run-bundle-schema.ts +89 -89
  115. package/src/extension/run-export.ts +73 -73
  116. package/src/extension/run-import.ts +84 -84
  117. package/src/extension/run-index.ts +94 -94
  118. package/src/extension/run-maintenance.ts +142 -142
  119. package/src/extension/session-summary.ts +8 -8
  120. package/src/extension/team-manager-command.ts +96 -95
  121. package/src/extension/team-recommendation.ts +188 -188
  122. package/src/extension/team-tool/api.ts +5 -2
  123. package/src/extension/team-tool/cancel.ts +224 -209
  124. package/src/extension/team-tool/config-patch.ts +36 -36
  125. package/src/extension/team-tool/context.ts +60 -60
  126. package/src/extension/team-tool/doctor.ts +242 -242
  127. package/src/extension/team-tool/handle-settings.ts +421 -195
  128. package/src/extension/team-tool/inspect.ts +41 -41
  129. package/src/extension/team-tool/lifecycle-actions.ts +139 -139
  130. package/src/extension/team-tool/parallel-dispatch.ts +156 -156
  131. package/src/extension/team-tool/plan.ts +19 -19
  132. package/src/extension/team-tool/respond.ts +112 -111
  133. package/src/extension/team-tool/run.ts +246 -228
  134. package/src/extension/team-tool/status.ts +110 -110
  135. package/src/extension/team-tool-types.ts +13 -13
  136. package/src/extension/team-tool.ts +16 -4
  137. package/src/extension/tool-result.ts +16 -16
  138. package/src/extension/validate-resources.ts +77 -77
  139. package/src/hooks/registry.ts +61 -61
  140. package/src/hooks/types.ts +40 -40
  141. package/src/i18n.ts +184 -184
  142. package/src/observability/correlation.ts +35 -35
  143. package/src/observability/event-to-metric.ts +68 -68
  144. package/src/observability/exporters/adapter.ts +30 -30
  145. package/src/observability/exporters/otlp-exporter.ts +106 -92
  146. package/src/observability/exporters/prometheus-exporter.ts +54 -54
  147. package/src/observability/metric-registry.ts +87 -87
  148. package/src/observability/metric-retention.ts +54 -54
  149. package/src/observability/metric-sink.ts +81 -56
  150. package/src/observability/metrics-primitives.ts +167 -167
  151. package/src/prompt/prompt-runtime.ts +72 -72
  152. package/src/runtime/adaptive-plan.ts +338 -0
  153. package/src/runtime/agent-control.ts +169 -169
  154. package/src/runtime/agent-memory.ts +72 -72
  155. package/src/runtime/agent-observability.ts +114 -114
  156. package/src/runtime/async-marker.ts +26 -26
  157. package/src/runtime/async-runner.ts +153 -79
  158. package/src/runtime/attention-events.ts +28 -28
  159. package/src/runtime/auto-resume.ts +100 -100
  160. package/src/runtime/background-runner.ts +122 -88
  161. package/src/runtime/cancellation.ts +61 -61
  162. package/src/runtime/capability-inventory.ts +116 -116
  163. package/src/runtime/child-pi-pool.ts +68 -0
  164. package/src/runtime/child-pi.ts +541 -463
  165. package/src/runtime/code-summary.ts +247 -247
  166. package/src/runtime/compaction-summary.ts +271 -271
  167. package/src/runtime/concurrency.ts +58 -58
  168. package/src/runtime/crash-recovery.ts +317 -301
  169. package/src/runtime/crew-agent-records.ts +379 -281
  170. package/src/runtime/crew-agent-runtime.ts +60 -60
  171. package/src/runtime/cross-extension-rpc.ts +72 -0
  172. package/src/runtime/custom-tools/irc-tool.ts +201 -201
  173. package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
  174. package/src/runtime/deadletter.ts +47 -47
  175. package/src/runtime/delivery-coordinator.ts +176 -176
  176. package/src/runtime/delta-conflict.ts +360 -360
  177. package/src/runtime/diagnostic-export.ts +102 -102
  178. package/src/runtime/direct-run.ts +35 -35
  179. package/src/runtime/effectiveness.ts +82 -81
  180. package/src/runtime/errors/crew-errors.ts +166 -0
  181. package/src/runtime/event-stream-bridge.ts +92 -92
  182. package/src/runtime/foreground-control.ts +82 -82
  183. package/src/runtime/green-contract.ts +46 -46
  184. package/src/runtime/group-join.ts +234 -106
  185. package/src/runtime/heartbeat-watcher.ts +145 -124
  186. package/src/runtime/iteration-hooks.ts +267 -264
  187. package/src/runtime/live-agent-control.ts +88 -88
  188. package/src/runtime/live-agent-manager.ts +377 -179
  189. package/src/runtime/live-control-realtime.ts +36 -36
  190. package/src/runtime/live-session-runtime.ts +676 -599
  191. package/src/runtime/loop-gates.ts +129 -129
  192. package/src/runtime/manifest-cache.ts +263 -263
  193. package/src/runtime/mcp-proxy.ts +113 -113
  194. package/src/runtime/metric-parser.ts +40 -40
  195. package/src/runtime/model-fallback.ts +282 -274
  196. package/src/runtime/model-resolver.ts +118 -0
  197. package/src/runtime/output-validator.ts +187 -187
  198. package/src/runtime/overflow-recovery.ts +175 -175
  199. package/src/runtime/parallel-research.ts +44 -44
  200. package/src/runtime/parallel-utils.ts +156 -156
  201. package/src/runtime/parent-guard.ts +80 -80
  202. package/src/runtime/phase-progress.ts +217 -217
  203. package/src/runtime/pi-args.ts +165 -165
  204. package/src/runtime/pi-json-output.ts +111 -111
  205. package/src/runtime/pi-spawn.ts +167 -167
  206. package/src/runtime/policy-engine.ts +79 -79
  207. package/src/runtime/post-checks.ts +125 -122
  208. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  209. package/src/runtime/process-status.ts +97 -73
  210. package/src/runtime/progress-event-coalescer.ts +43 -43
  211. package/src/runtime/recovery-recipes.ts +74 -74
  212. package/src/runtime/retry-executor.ts +81 -81
  213. package/src/runtime/role-permission.ts +39 -39
  214. package/src/runtime/run-tracker.ts +99 -0
  215. package/src/runtime/runtime-policy.ts +21 -0
  216. package/src/runtime/runtime-resolver.ts +94 -90
  217. package/src/runtime/scheduler.ts +294 -0
  218. package/src/runtime/semaphore.ts +131 -131
  219. package/src/runtime/sensitive-paths.ts +92 -92
  220. package/src/runtime/session-usage.ts +79 -79
  221. package/src/runtime/settings-store.ts +103 -0
  222. package/src/runtime/sidechain-output.ts +29 -29
  223. package/src/runtime/skill-instructions.ts +222 -222
  224. package/src/runtime/stale-reconciler.ts +198 -189
  225. package/src/runtime/streaming-output.ts +47 -0
  226. package/src/runtime/subagent-manager.ts +404 -395
  227. package/src/runtime/subprocess-tool-registry.ts +67 -67
  228. package/src/runtime/task-display.ts +38 -38
  229. package/src/runtime/task-graph-scheduler.ts +122 -122
  230. package/src/runtime/task-graph.ts +207 -207
  231. package/src/runtime/task-output-context.ts +177 -177
  232. package/src/runtime/task-packet.ts +93 -93
  233. package/src/runtime/task-quality.ts +207 -207
  234. package/src/runtime/task-runner/capabilities.ts +78 -78
  235. package/src/runtime/task-runner/live-executor.ts +131 -113
  236. package/src/runtime/task-runner/progress.ts +119 -119
  237. package/src/runtime/task-runner/prompt-builder.ts +139 -139
  238. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  239. package/src/runtime/task-runner/result-utils.ts +14 -14
  240. package/src/runtime/task-runner/run-projection.ts +103 -103
  241. package/src/runtime/task-runner/state-helpers.ts +22 -22
  242. package/src/runtime/task-runner.ts +469 -458
  243. package/src/runtime/team-runner.ts +693 -945
  244. package/src/runtime/usage-tracker.ts +71 -0
  245. package/src/runtime/worker-heartbeat.ts +21 -21
  246. package/src/runtime/worker-startup.ts +57 -57
  247. package/src/runtime/workflow-state.ts +187 -187
  248. package/src/runtime/yield-handler.ts +190 -189
  249. package/src/schema/config-schema.ts +172 -168
  250. package/src/schema/team-tool-schema.ts +126 -125
  251. package/src/schema/validation-types.ts +151 -148
  252. package/src/skills/discover-skills.ts +67 -67
  253. package/src/skills/skill-templates.ts +374 -374
  254. package/src/state/active-run-registry.ts +227 -191
  255. package/src/state/artifact-store.ts +130 -129
  256. package/src/state/atomic-write.ts +262 -178
  257. package/src/state/blob-store.ts +116 -116
  258. package/src/state/contracts.ts +111 -111
  259. package/src/state/event-log-rotation.ts +161 -158
  260. package/src/state/event-log.ts +383 -240
  261. package/src/state/event-reconstructor.ts +217 -217
  262. package/src/state/jsonl-writer.ts +82 -82
  263. package/src/state/locks.ts +146 -148
  264. package/src/state/mailbox.ts +446 -405
  265. package/src/state/state-store.ts +364 -351
  266. package/src/state/task-claims.ts +44 -44
  267. package/src/state/types.ts +285 -285
  268. package/src/state/usage.ts +29 -29
  269. package/src/subagents/async-entry.ts +1 -1
  270. package/src/subagents/index.ts +3 -3
  271. package/src/subagents/live/control.ts +1 -1
  272. package/src/subagents/live/manager.ts +1 -1
  273. package/src/subagents/live/realtime.ts +1 -1
  274. package/src/subagents/live/session-runtime.ts +1 -1
  275. package/src/subagents/manager.ts +1 -1
  276. package/src/subagents/spawn.ts +1 -1
  277. package/src/teams/discover-teams.ts +116 -116
  278. package/src/teams/team-config.ts +27 -27
  279. package/src/teams/team-serializer.ts +38 -38
  280. package/src/types/diff.d.ts +18 -18
  281. package/src/ui/agent-management-overlay.ts +144 -144
  282. package/src/ui/crew-widget.ts +487 -370
  283. package/src/ui/dashboard-panes/agents-pane.ts +109 -28
  284. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  285. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  286. package/src/ui/dashboard-panes/health-pane.ts +30 -30
  287. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  288. package/src/ui/dashboard-panes/progress-pane.ts +30 -30
  289. package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
  290. package/src/ui/heartbeat-aggregator.ts +63 -63
  291. package/src/ui/keybinding-map.ts +97 -94
  292. package/src/ui/live-conversation-overlay.ts +152 -0
  293. package/src/ui/live-run-sidebar.ts +180 -180
  294. package/src/ui/mascot.ts +442 -442
  295. package/src/ui/overlays/agent-picker-overlay.ts +57 -57
  296. package/src/ui/overlays/confirm-overlay.ts +58 -58
  297. package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
  298. package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
  299. package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
  300. package/src/ui/pi-ui-compat.ts +57 -57
  301. package/src/ui/powerbar-publisher.ts +221 -197
  302. package/src/ui/render-scheduler.ts +216 -143
  303. package/src/ui/run-action-dispatcher.ts +118 -117
  304. package/src/ui/run-dashboard.ts +526 -464
  305. package/src/ui/run-event-bus.ts +208 -208
  306. package/src/ui/run-snapshot-cache.ts +826 -777
  307. package/src/ui/settings-overlay.ts +721 -0
  308. package/src/ui/snapshot-types.ts +86 -70
  309. package/src/ui/theme-adapter.ts +190 -190
  310. package/src/ui/tool-progress-formatter.ts +89 -0
  311. package/src/ui/transcript-cache.ts +94 -94
  312. package/src/ui/transcript-viewer.ts +335 -335
  313. package/src/utils/conflict-detect.ts +662 -0
  314. package/src/utils/env-filter.ts +30 -0
  315. package/src/utils/file-coalescer.ts +86 -86
  316. package/src/utils/frontmatter.ts +68 -68
  317. package/src/utils/fs-watch.ts +88 -31
  318. package/src/utils/gh-protocol.ts +479 -0
  319. package/src/utils/ids.ts +17 -17
  320. package/src/utils/incremental-reader.ts +104 -104
  321. package/src/utils/internal-error.ts +6 -6
  322. package/src/utils/names.ts +27 -27
  323. package/src/utils/paths.ts +102 -63
  324. package/src/utils/redaction.ts +44 -44
  325. package/src/utils/resolve-shell.ts +34 -0
  326. package/src/utils/safe-paths.ts +47 -47
  327. package/src/utils/scan-cache.ts +136 -136
  328. package/src/utils/sleep.ts +2 -1
  329. package/src/utils/sse-parser.ts +134 -134
  330. package/src/utils/task-name-generator.ts +337 -337
  331. package/src/utils/timings.ts +33 -33
  332. package/src/utils/visual.ts +243 -198
  333. package/src/workflows/discover-workflows.ts +139 -139
  334. package/src/workflows/validate-workflow.ts +40 -40
  335. package/src/workflows/workflow-config.ts +26 -26
  336. package/src/workflows/workflow-serializer.ts +32 -32
  337. package/src/worktree/branch-freshness.ts +45 -45
  338. package/src/worktree/cleanup.ts +75 -72
  339. package/src/worktree/worktree-manager.ts +188 -146
  340. package/teams/default.team.md +12 -12
  341. package/teams/fast-fix.team.md +11 -11
  342. package/teams/implementation.team.md +18 -18
  343. package/teams/parallel-research.team.md +14 -14
  344. package/teams/research.team.md +11 -11
  345. package/teams/review.team.md +12 -12
  346. package/tsconfig.json +19 -19
  347. package/workflows/default.workflow.md +30 -30
  348. package/workflows/fast-fix.workflow.md +23 -23
  349. package/workflows/implementation.workflow.md +43 -43
  350. package/workflows/parallel-research.workflow.md +46 -46
  351. package/workflows/research.workflow.md +22 -22
  352. package/workflows/review.workflow.md +30 -30
  353. package/skills/task-packet/SKILL.md +0 -28
  354. package/skills/verify-evidence/SKILL.md +0 -27
@@ -1,599 +1,676 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import type { AgentConfig } from "../agents/agent-config.ts";
4
- import type { CrewRuntimeConfig } from "../config/config.ts";
5
- import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
6
- import { buildMemoryBlock } from "./agent-memory.ts";
7
- import { registerLiveAgent, updateLiveAgentStatus } from "./live-agent-manager.ts";
8
- import { applyLiveAgentControlRequest, applyLiveAgentControlRequests, type LiveAgentControlCursor } from "./live-agent-control.ts";
9
- import { subscribeLiveControlRealtime } from "./live-control-realtime.ts";
10
- import { eventToSidechainType, sidechainOutputPath, writeSidechainEntry } from "./sidechain-output.ts";
11
- import type { WorkflowStep } from "../workflows/workflow-config.ts";
12
- import { isLiveSessionRuntimeAvailable } from "./runtime-resolver.ts";
13
- import { redactSecrets } from "../utils/redaction.ts";
14
- import { buildConfiguredModelRouting } from "./model-fallback.ts";
15
- import { DEFAULT_LIVE_SESSION } from "../config/defaults.ts";
16
- import { buildYieldReminder, hasYieldInOutput, isYieldEvent, extractYieldResult, validateYieldData, DEFAULT_YIELD_CONFIG, type YieldResult } from "./yield-handler.ts";
17
- import { buildMcpProxyFromSession } from "./mcp-proxy.ts";
18
- import { createSubmitResultTool } from "./custom-tools/submit-result-tool.ts";
19
- import { createIrcTool } from "./custom-tools/irc-tool.ts";
20
- import { buildExtensionBridge } from "./live-extension-bridge.ts";
21
- import { logInternalError } from "../utils/internal-error.ts";
22
- // prose-compressor imported for custom tool descriptions below;
23
- // tool description compression for SDK-managed tools awaits SDK support.
24
- import { compressToolDescription } from "./prose-compressor.ts";
25
- import { buildSensitivePathConstraint } from "./sensitive-paths.ts";
26
- import { collectLiveSessionHealth, formatLiveSessionDiagnostics, type LiveSessionHealth } from "./live-session-health.ts";
27
- import { listLiveAgents } from "./live-agent-manager.ts";
28
-
29
- export interface LiveSessionSpawnInput {
30
- manifest: TeamRunManifest;
31
- task: TeamTaskState;
32
- step: WorkflowStep;
33
- agent: AgentConfig;
34
- prompt: string;
35
- signal?: AbortSignal;
36
- transcriptPath?: string;
37
- onEvent?: (event: unknown) => void;
38
- onOutput?: (text: string) => void;
39
- runtimeConfig?: CrewRuntimeConfig;
40
- parentContext?: string;
41
- parentModel?: unknown;
42
- modelRegistry?: unknown;
43
- modelOverride?: string;
44
- teamRoleModel?: string;
45
- isCurrent?: () => boolean;
46
- /** Phase 2: Output schema for validating yield data. */
47
- outputSchema?: unknown;
48
- }
49
-
50
- export interface LiveSessionRunResult {
51
- available: true;
52
- exitCode: number | null;
53
- stdout: string;
54
- stderr: string;
55
- jsonEvents: number;
56
- usage?: UsageState;
57
- error?: string;
58
- /** Phase 1: Extracted yield result from submit_result tool call. */
59
- yieldResult?: YieldResult;
60
- }
61
-
62
- export interface LiveSessionUnavailableResult {
63
- available: false;
64
- reason: string;
65
- }
66
-
67
- export interface LiveSessionPlannedResult {
68
- available: true;
69
- reason: string;
70
- }
71
-
72
- type LiveSessionModule = Record<string, unknown> & {
73
- createAgentSession?: (options?: Record<string, unknown>) => Promise<{ session: LiveSessionLike; modelFallbackMessage?: string }>;
74
- DefaultResourceLoader?: new (options: Record<string, unknown>) => { reload?: () => Promise<void> };
75
- SessionManager?: { inMemory?: (cwd?: string) => unknown; create?: (cwd?: string, sessionDir?: string) => unknown };
76
- SettingsManager?: { create?: (cwd?: string, agentDir?: string) => unknown };
77
- getAgentDir?: () => string;
78
- };
79
-
80
- type LiveSessionLike = {
81
- subscribe?: (listener: (event: unknown) => void) => (() => void);
82
- prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>;
83
- steer?: (text: string) => Promise<void>;
84
- abort?: () => Promise<void> | void;
85
- getStats?: () => unknown;
86
- stats?: unknown;
87
- bindExtensions?: (bindings?: Record<string, unknown>) => Promise<void>;
88
- getActiveToolNames?: () => string[];
89
- setActiveToolsByName?: (names: string[]) => void;
90
- };
91
-
92
- function appendTranscript(filePath: string | undefined, event: unknown): void {
93
- if (!filePath) return;
94
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
95
- fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets(event))}\n`, "utf-8");
96
- }
97
-
98
- function asRecord(value: unknown): Record<string, unknown> | undefined {
99
- return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
100
- }
101
-
102
- function textFromContent(content: unknown): string[] {
103
- if (typeof content === "string") return [content];
104
- if (!Array.isArray(content)) return [];
105
- return content.flatMap((part) => {
106
- const obj = asRecord(part);
107
- if (!obj) return [];
108
- if (obj.type === "text" && typeof obj.text === "string") return [obj.text];
109
- if (typeof obj.content === "string") return [obj.content];
110
- return [];
111
- });
112
- }
113
-
114
- function eventText(event: unknown): string[] {
115
- const obj = asRecord(event);
116
- if (!obj) return [];
117
- const text: string[] = [];
118
- if (typeof obj.text === "string") text.push(obj.text);
119
- text.push(...textFromContent(obj.content));
120
- const message = asRecord(obj.message);
121
- if (message) text.push(...textFromContent(message.content));
122
- return text.filter((entry) => entry.trim());
123
- }
124
-
125
- function finalAssistantText(event: unknown): string[] {
126
- const obj = asRecord(event);
127
- if (!obj || obj.type !== "message_end") return [];
128
- const message = asRecord(obj.message);
129
- if (message?.role !== "assistant") return [];
130
- return textFromContent(message.content);
131
- }
132
-
133
- function numberField(obj: Record<string, unknown> | undefined, keys: string[]): number | undefined {
134
- if (!obj) return undefined;
135
- for (const key of keys) {
136
- const value = obj[key];
137
- if (typeof value === "number" && Number.isFinite(value)) return value;
138
- }
139
- return undefined;
140
- }
141
-
142
- function modelFromRegistry(modelRegistry: unknown, modelId: string | undefined): unknown {
143
- if (!modelId || !modelId.includes("/")) return undefined;
144
- const registry = asRecord(modelRegistry);
145
- const find = registry?.find;
146
- if (typeof find !== "function") return undefined;
147
- const [provider, ...modelParts] = modelId.split("/");
148
- const id = modelParts.join("/");
149
- try {
150
- return find.call(modelRegistry, provider, id);
151
- } catch {
152
- return undefined;
153
- }
154
- }
155
-
156
- /** Communication intensity by role (caveman-inspired token optimization) */
157
- const ROLE_INTENSITY: Record<string, "lite" | "full" | "ultra"> = {
158
- explorer: "ultra",
159
- analyst: "full",
160
- planner: "full",
161
- critic: "full",
162
- executor: "full",
163
- reviewer: "full",
164
- "security-reviewer": "full",
165
- "test-engineer": "full",
166
- verifier: "full",
167
- writer: "lite",
168
- };
169
-
170
- function buildCommunicationStyle(role: string): string {
171
- const intensity = ROLE_INTENSITY[role] ?? "full";
172
- if (intensity === "lite") return "## Communication\nProfessional concise. No filler/hedging. Full sentences OK.";
173
- if (intensity === "ultra") return [
174
- "## Communication (ultra-compressed)",
175
- "Drop: articles, filler, hedging, pleasantries. Fragments OK.",
176
- "Pattern: [thing] [action] [reason].",
177
- "Code/paths/symbols: exact, never abbreviated. Errors quoted exact.",
178
- "Abbreviate prose words: DB/auth/config/req/res/fn/impl.",
179
- "Arrows for causality: X Y. One word when one word enough.",
180
- "Security/destructive: write normal English. Resume compressed after.",
181
- ].join("\n");
182
- return [
183
- "## Communication (compressed)",
184
- "Drop: articles (a/an/the), filler (just/really/basically/actually/simply), hedging, pleasantries.",
185
- "Short synonyms. Fragments OK. Pattern: [thing] [action] [reason]. [next step].",
186
- "Code/paths/symbols: exact. Errors quoted exact.",
187
- "Security/destructive: write normal English. Resume compressed after.",
188
- ].join("\n");
189
- }
190
-
191
- function buildOutputContract(role: string): string {
192
- if (role === "explorer") return [
193
- "## Output Contract",
194
- "<path>:<line> — `<symbol>` — <≤6 word note>",
195
- "Group: Defs: / Refs: / Callers: / Tests: / Sites:",
196
- "Zero hits → \"No match.\"",
197
- "Last line → totals: N defs, M refs.",
198
- ].join("\n");
199
- if (role === "executor") return [
200
- "## Output Contract",
201
- "<path>:<line-range> <change ≤10 words>.",
202
- "verified: <re-read OK | mismatch @ path:line>.",
203
- "Refusal tokens: too-big. / needs-confirm. / ambiguous. / regressed.",
204
- ].join("\n");
205
- if (role === "reviewer" || role === "security-reviewer") return [
206
- "## Output Contract",
207
- "<path>:<line>: <emoji> <severity>: <problem>. <fix>.",
208
- "Severity: 🔴 bug, 🟡 risk, 🔵 nit, ❓ question.",
209
- "Zero findings \"No issues.\"",
210
- "Sorted: file order → ascending line numbers.",
211
- ].join("\n");
212
- if (role === "verifier") return [
213
- "## Output Contract",
214
- "PASS: <what verified> <evidence ≤20 words>.",
215
- "FAIL: <what failed> <reason>. <expected vs actual>.",
216
- "Evidence: file paths, test output, or diffs.",
217
- ].join("\n");
218
- if (role === "writer") return "## Output Contract\nWrite clear documentation. Full sentences. No compression.";
219
- return ""; // planner, critic, analyst, test-engineer: no strict format
220
- }
221
-
222
- /**
223
- * Phase 3 (caveman): Compress tool descriptions in a live session to reduce
224
- * input token cost per tool call. MCP tools often have verbose descriptions
225
- * (e.g. "This tool allows you to search for files in the filesystem..." → "Search files in filesystem.").
226
- * Compresses only description text, never modifies tool names or parameters.
227
- */
228
- function compressSessionToolDescriptions(session: LiveSessionLike): void {
229
- if (typeof session.getActiveToolNames !== "function") return;
230
- // The Pi SDK doesn't expose a setDescription API, but we can attempt
231
- // to compress via setActiveToolsByName if the session supports it.
232
- // For now, this is a no-op that documents the intent for future SDK support.
233
- // When Pi SDK adds tool description mutation, this function will compress.
234
- // Side benefit: the import of compressToolDescription ensures the module
235
- // is loaded and tree-shakeable, so adding the actual logic later is trivial.
236
- }
237
-
238
- function liveSystemPrompt(input: LiveSessionSpawnInput): string {
239
- const memory = input.agent.memory ? buildMemoryBlock(input.agent.name, input.agent.memory, input.task.cwd, Boolean(input.agent.tools?.some((tool) => tool === "write" || tool === "edit"))) : "";
240
- const role = input.task.role;
241
- const styleBlock = buildCommunicationStyle(role);
242
- const contractBlock = buildOutputContract(role);
243
- const sensitiveConstraint = buildSensitivePathConstraint();
244
- return [
245
- "# pi-crew Live Subagent",
246
- `Run ID: ${input.manifest.runId}`,
247
- `Task ID: ${input.task.id}`,
248
- `Role: ${role}`,
249
- `Agent: ${input.agent.name}`,
250
- `Working directory: ${input.task.cwd}`,
251
- "",
252
- styleBlock,
253
- contractBlock,
254
- sensitiveConstraint,
255
- "",
256
- input.agent.systemPrompt || "Follow the user task exactly and report verification evidence.",
257
- memory ? `\n${memory}` : "",
258
- ].filter(Boolean).join("\n");
259
- }
260
-
261
- function filterActiveTools(session: LiveSessionLike, agent: AgentConfig): void {
262
- if (typeof session.getActiveToolNames !== "function" || typeof session.setActiveToolsByName !== "function") return;
263
- const recursiveTools = new Set(["team", "Team", "Agent", "get_subagent_result", "steer_subagent"]);
264
- const allowed = agent.tools?.length ? new Set(agent.tools) : undefined;
265
- const active = session.getActiveToolNames().filter((name) => !recursiveTools.has(name) && (!allowed || allowed.has(name)));
266
- session.setActiveToolsByName(active);
267
- }
268
-
269
- function usageFromStats(stats: unknown): UsageState | undefined {
270
- const obj = asRecord(stats);
271
- if (!obj) return undefined;
272
- const input = numberField(obj, ["input", "inputTokens", "input_tokens"]);
273
- const output = numberField(obj, ["output", "outputTokens", "output_tokens"]);
274
- const cacheRead = numberField(obj, ["cacheRead", "cache_read"]);
275
- const cacheWrite = numberField(obj, ["cacheWrite", "cache_write"]);
276
- const cost = numberField(obj, ["cost"]);
277
- const turns = numberField(obj, ["turns", "turnCount", "turn_count"]);
278
- return [input, output, cacheRead, cacheWrite, cost, turns].some((value) => value !== undefined) ? { input, output, cacheRead, cacheWrite, cost, turns } : undefined;
279
- }
280
-
281
- export async function probeLiveSessionRuntime(): Promise<LiveSessionUnavailableResult | LiveSessionPlannedResult> {
282
- const availability = await isLiveSessionRuntimeAvailable();
283
- if (!availability.available) return { available: false, reason: availability.reason ?? "Live-session runtime is unavailable." };
284
- return { available: true, reason: "Live-session SDK exports are available. pi-crew can run in-process live agents when runtime.mode=live-session." };
285
- }
286
-
287
- export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<LiveSessionRunResult> {
288
- const isCurrent = input.isCurrent ?? (() => true);
289
-
290
- // G1: Capture yield result from custom tool callback
291
- let customToolYieldResult: YieldResult | undefined;
292
- let customToolYieldResolved = false;
293
- if (process.env.PI_CREW_MOCK_LIVE_SESSION === "success") {
294
- const agentId = `${input.manifest.runId}:${input.task.id}`;
295
- const inherited = input.runtimeConfig?.inheritContext === true && input.parentContext ? ` with inherited context: ${input.parentContext}` : "";
296
- const event = { type: "message_end", message: { role: "assistant", content: [{ type: "text", text: `Mock live-session success for ${input.agent.name}${inherited}` }] } };
297
- const mockSession = { steer: async () => {}, prompt: async () => {}, abort: async () => {} };
298
- registerLiveAgent({ agentId, runId: input.manifest.runId, taskId: input.task.id, session: mockSession, status: "running" });
299
- appendTranscript(input.transcriptPath, event);
300
- const sidechainPath = sidechainOutputPath(input.manifest.stateRoot, input.task.id);
301
- writeSidechainEntry(sidechainPath, { agentId, type: "user", message: { role: "user", content: input.prompt }, cwd: input.task.cwd });
302
- writeSidechainEntry(sidechainPath, { agentId, type: "message", message: event, cwd: input.task.cwd });
303
- if (isCurrent()) input.onEvent?.(event);
304
- const stdout = `Mock live-session success for ${input.agent.name}${inherited}`;
305
- if (isCurrent()) input.onOutput?.(stdout);
306
- updateLiveAgentStatus(agentId, "completed");
307
- return { available: true, exitCode: 0, stdout, stderr: "", jsonEvents: 1 };
308
- }
309
- const availability = await isLiveSessionRuntimeAvailable();
310
- if (!availability.available) return { available: true, exitCode: 1, stdout: "", stderr: availability.reason ?? "Live-session runtime unavailable.", jsonEvents: 0, error: availability.reason };
311
- const mod = await import("@mariozechner/pi-coding-agent") as LiveSessionModule;
312
- if (typeof mod.createAgentSession !== "function") return { available: true, exitCode: 1, stdout: "", stderr: "createAgentSession export is unavailable.", jsonEvents: 0, error: "createAgentSession export is unavailable." };
313
- let session: LiveSessionLike | undefined;
314
- let unsubscribe: (() => void) | undefined;
315
- let unsubscribeControlRealtime: (() => void) | undefined;
316
- let controlTimer: ReturnType<typeof setInterval> | undefined;
317
- let stdout = "";
318
- let jsonEvents = 0;
319
- const collectedJsonEvents: Record<string, unknown>[] = [];
320
- let yieldResult: YieldResult | undefined;
321
- try {
322
- const agentDir = typeof mod.getAgentDir === "function" ? mod.getAgentDir() : undefined;
323
- let resourceLoader: unknown;
324
- if (mod.DefaultResourceLoader && agentDir) {
325
- resourceLoader = new mod.DefaultResourceLoader({
326
- cwd: input.task.cwd,
327
- agentDir,
328
- noPromptTemplates: true,
329
- noThemes: true,
330
- noContextFiles: input.runtimeConfig?.inheritContext !== true,
331
- systemPromptOverride: () => liveSystemPrompt(input),
332
- appendSystemPromptOverride: () => [],
333
- });
334
- await (resourceLoader as { reload?: () => Promise<void> }).reload?.();
335
- }
336
- const modelRouting = buildConfiguredModelRouting({ overrideModel: input.modelOverride, stepModel: input.step.model, teamRoleModel: input.teamRoleModel, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: input.manifest.cwd });
337
- const resolvedModel = modelFromRegistry(input.modelRegistry, modelRouting.candidates[0] ?? modelRouting.requested) ?? input.parentModel;
338
- // Phase 4: MCP proxy will be determined after session creation
339
- // (we check parent's MCP tools and share connections when available)
340
- const mcpProxy = buildMcpProxyFromSession([], { shareMcp: true });
341
-
342
- // G1: Build custom tools (submit_result + irc)
343
- const agentId = `${input.manifest.runId}:${input.task.id}`;
344
- const submitResultTool = createSubmitResultTool((result) => {
345
- customToolYieldResult = result;
346
- customToolYieldResolved = true;
347
- });
348
- const ircTool = createIrcTool(agentId);
349
- const customTools = [submitResultTool, ircTool];
350
-
351
- const created = await mod.createAgentSession({
352
- cwd: input.task.cwd,
353
- ...(agentDir ? { agentDir } : {}),
354
- ...(resourceLoader ? { resourceLoader } : {}),
355
- ...(mod.SessionManager?.inMemory ? { sessionManager: mod.SessionManager.inMemory(input.task.cwd) } : {}),
356
- ...(mod.SettingsManager?.create && agentDir ? { settingsManager: mod.SettingsManager.create(input.task.cwd, agentDir) } : {}),
357
- ...(input.modelRegistry ? { modelRegistry: input.modelRegistry } : {}),
358
- ...(resolvedModel ? { model: resolvedModel } : {}),
359
- ...(input.agent.thinking ? { thinkingLevel: input.agent.thinking } : {}),
360
- ...(mcpProxy.enableMcp ? {} : { enableMCP: false }),
361
- customTools,
362
- });
363
- session = created.session;
364
- filterActiveTools(session, input.agent);
365
- await session.bindExtensions?.({});
366
-
367
- // Phase 3 (caveman): Compress tool descriptions to reduce input token cost
368
- compressSessionToolDescriptions(session);
369
-
370
- // Phase 5: Initialize extension runner bridge if available
371
- // The bridge provides extension-like APIs (sendMessage, setActiveTools, etc.)
372
- // to the extension runner if the session exposes one.
373
- const extensionBridge = buildExtensionBridge(session as never);
374
- if (extensionBridge) {
375
- const extRunner = (session as Record<string, unknown>).extensionRunner;
376
- if (extRunner && typeof (extRunner as Record<string, unknown>).initialize === "function") {
377
- try {
378
- (extRunner as { initialize: (apis: unknown, host: unknown) => void }).initialize(extensionBridge.apis, extensionBridge.host);
379
- if (typeof (extRunner as Record<string, unknown>).emit === "function") {
380
- await (extRunner as { emit: (event: unknown) => Promise<void> }).emit({ type: "session_start" });
381
- }
382
- } catch {
383
- // Extension runner initialization failure should not block the session
384
- }
385
- }
386
- }
387
-
388
- registerLiveAgent({ agentId, runId: input.manifest.runId, taskId: input.task.id, session, status: "running" });
389
- let controlCursor: LiveAgentControlCursor = { offset: 0 };
390
- const seenControlRequestIds = new Set<string>();
391
- let controlBusy = false;
392
- const pollControl = async () => {
393
- if (!isCurrent() || controlBusy || !session) return;
394
- controlBusy = true;
395
- try {
396
- controlCursor = await applyLiveAgentControlRequests({ manifest: input.manifest, taskId: input.task.id, agentId, session, cursor: controlCursor, seenRequestIds: seenControlRequestIds });
397
- } finally {
398
- controlBusy = false;
399
- }
400
- };
401
- unsubscribeControlRealtime = subscribeLiveControlRealtime((request) => {
402
- if (!isCurrent() || request.runId !== input.manifest.runId || request.taskId !== input.task.id || !session) return;
403
- void applyLiveAgentControlRequest({ request, taskId: input.task.id, agentId, session, seenRequestIds: seenControlRequestIds });
404
- });
405
- await pollControl();
406
- controlTimer = setInterval(() => {
407
- if (isCurrent()) void pollControl();
408
- }, 500);
409
- let turnCount = 0;
410
- let softLimitReached = false;
411
- const maxTurns = input.runtimeConfig?.maxTurns;
412
- const graceTurns = input.runtimeConfig?.graceTurns ?? 5;
413
- const sidechainPath = sidechainOutputPath(input.manifest.stateRoot, input.task.id);
414
- writeSidechainEntry(sidechainPath, { agentId, type: "user", message: { role: "user", content: input.prompt }, cwd: input.task.cwd });
415
- if (typeof session.subscribe === "function") {
416
- unsubscribe = session.subscribe((event) => {
417
- if (!isCurrent()) return;
418
- jsonEvents += 1;
419
- appendTranscript(input.transcriptPath, event);
420
- const sidechainType = eventToSidechainType(event);
421
- if (sidechainType) writeSidechainEntry(sidechainPath, { agentId, type: sidechainType, message: event, cwd: input.task.cwd });
422
- const obj = asRecord(event);
423
- if (obj?.type === "turn_end") {
424
- turnCount += 1;
425
- if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
426
- softLimitReached = true;
427
- void session?.steer?.("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
428
- } else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
429
- void session?.abort?.();
430
- }
431
- }
432
- input.onEvent?.(event);
433
- const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
434
- if (text.trim()) {
435
- stdout += `${text}\n`;
436
- input.onOutput?.(text);
437
- }
438
- // Phase 1: collect events for yield detection
439
- if (event && typeof event === "object" && !Array.isArray(event)) {
440
- collectedJsonEvents.push(event as Record<string, unknown>);
441
- }
442
- });
443
- }
444
- if (input.signal) {
445
- if (input.signal.aborted) await session.abort?.();
446
- else input.signal.addEventListener("abort", () => { void session?.abort?.(); }, { once: true });
447
- }
448
- const effectivePrompt = input.runtimeConfig?.inheritContext === true && input.parentContext ? `${input.parentContext}\n\n---\n# Live Subagent Task\n${input.prompt}` : input.prompt;
449
-
450
- // Phase 3: Wrap session.prompt with timeout for graceful cancellation
451
- const sessionTimeoutMs = DEFAULT_LIVE_SESSION.responseTimeoutMs;
452
- const promptPromise = session.prompt?.(effectivePrompt, { source: "api", expandPromptTemplates: false });
453
- if (promptPromise) {
454
- const timeoutPromise = new Promise<void>((_, reject) => {
455
- const timer = setTimeout(() => reject(new Error(`Live-session timed out after ${sessionTimeoutMs}ms`)), sessionTimeoutMs);
456
- timer.unref();
457
- input.signal?.addEventListener("abort", () => clearTimeout(timer), { once: true });
458
- });
459
- try {
460
- await Promise.race([promptPromise, timeoutPromise]);
461
- } catch (promptError) {
462
- const msg = promptError instanceof Error ? promptError.message : String(promptError);
463
- if (msg.includes("timed out")) {
464
- await session.abort?.();
465
- updateLiveAgentStatus(agentId, "failed");
466
- return { available: true, exitCode: 1, stdout: stdout.trim(), stderr: msg, jsonEvents, error: msg };
467
- }
468
- throw promptError;
469
- }
470
- }
471
-
472
- // --- Phase 1: Yield enforcement loop ---
473
- // After the initial prompt completes, check if the worker called submit_result.
474
- // Priority: 1) custom tool callback (G1), 2) JSON event detection (legacy).
475
- const yieldConfig = input.runtimeConfig?.yield ?? { enabled: DEFAULT_YIELD_CONFIG.enabled };
476
- const yieldEnabled = yieldConfig.enabled !== false;
477
- if (yieldEnabled && session) {
478
- // Check custom tool callback first (G1)
479
- if (customToolYieldResolved && customToolYieldResult) {
480
- yieldResult = customToolYieldResult;
481
- } else {
482
- // Legacy: detect from JSON events
483
- const alreadyYielded = hasYieldInOutput(collectedJsonEvents);
484
- if (alreadyYielded) {
485
- const yieldEvent = collectedJsonEvents.find((e) => isYieldEvent(e));
486
- if (yieldEvent) yieldResult = extractYieldResult(yieldEvent);
487
- }
488
- }
489
- // Phase 2: Validate yield data against output schema if provided
490
- let schemaFailures = 0;
491
- const maxSchemaFailures = 2;
492
- if (yieldResult && input.outputSchema) {
493
- const validation = await validateYieldData(yieldResult.structuredData, input.outputSchema);
494
- if (!validation.valid) {
495
- schemaFailures++;
496
- yieldResult = undefined;
497
- customToolYieldResolved = false;
498
- const schemaReminder = `Your submit_result data did not match the required schema: ${validation.error}. Please fix and call submit_result again with valid data.`;
499
- try {
500
- await session.prompt?.(schemaReminder, { source: "api", expandPromptTemplates: false });
501
- } catch {
502
- /* ignore */
503
- }
504
- await new Promise((resolve) => setTimeout(resolve, DEFAULT_LIVE_SESSION.yieldPollIntervalMs));
505
- // Check again after schema reminder
506
- if (customToolYieldResolved && customToolYieldResult) {
507
- yieldResult = customToolYieldResult;
508
- } else {
509
- const newEvents = collectedJsonEvents.slice(-10);
510
- if (hasYieldInOutput(newEvents)) {
511
- const yieldEvent = newEvents.find((e) => isYieldEvent(e));
512
- if (yieldEvent) {
513
- const candidate = extractYieldResult(yieldEvent);
514
- if (candidate && input.outputSchema) {
515
- const revalidation = await validateYieldData(candidate.structuredData, input.outputSchema);
516
- if (revalidation.valid || schemaFailures >= maxSchemaFailures) {
517
- yieldResult = candidate;
518
- }
519
- }
520
- }
521
- }
522
- }
523
- }
524
- }
525
- // Reminder loop — only if yield not yet received
526
- const maxReminders = yieldConfig.maxReminders ?? DEFAULT_LIVE_SESSION.maxYieldRetries;
527
- let retryCount = 0;
528
- while (!customToolYieldResolved && !yieldResult && retryCount < maxReminders && !input.signal?.aborted) {
529
- retryCount++;
530
- const reminder = buildYieldReminder(retryCount, maxReminders, yieldConfig.reminderPrompt);
531
- try {
532
- // G6: Constrain tool set to submit_result before sending reminder
533
- const prevTools = typeof session.getActiveToolNames === "function" ? session.getActiveToolNames() : [];
534
- if (typeof session.setActiveToolsByName === "function" && prevTools.length > 0) {
535
- session.setActiveToolsByName(["submit_result"]);
536
- }
537
- await session.prompt?.(reminder, { source: "api", expandPromptTemplates: false });
538
- // Restore previous tools
539
- if (typeof session.setActiveToolsByName === "function" && prevTools.length > 0) {
540
- session.setActiveToolsByName(prevTools);
541
- }
542
- } catch {
543
- break;
544
- }
545
- const pollInterval = DEFAULT_LIVE_SESSION.yieldPollIntervalMs;
546
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
547
- // Check custom tool callback
548
- if (customToolYieldResolved && customToolYieldResult) {
549
- yieldResult = customToolYieldResult;
550
- break;
551
- }
552
- // Legacy: check JSON events
553
- if (hasYieldInOutput(collectedJsonEvents.slice(-10))) {
554
- const yieldEvent = collectedJsonEvents.slice(-10).find((e) => isYieldEvent(e));
555
- if (yieldEvent) yieldResult = extractYieldResult(yieldEvent);
556
- break;
557
- }
558
- }
559
- if (!customToolYieldResolved && !yieldResult && !input.signal?.aborted && retryCount >= maxReminders) {
560
- input.onEvent?.({ type: "task.attention", runId: input.manifest.runId, taskId: input.task.id, message: "Live-session worker completed without calling submit_result tool.", data: { activityState: "needs_attention", reason: "no_yield", attempts: retryCount } });
561
- }
562
- }
563
-
564
- const usage = usageFromStats(typeof session.getStats === "function" ? session.getStats() : session.stats);
565
- updateLiveAgentStatus(agentId, "completed");
566
- return { available: true, exitCode: 0, stdout: stdout.trim(), stderr: created.modelFallbackMessage ?? "", jsonEvents, usage, yieldResult };
567
- } catch (error) {
568
- const message = error instanceof Error ? error.message : String(error);
569
-
570
- // Phase 8: Log diagnostics on failure
571
- try {
572
- const agents = listLiveAgents();
573
- const health = collectLiveSessionHealth(agents, () => undefined);
574
- const diagnostics = formatLiveSessionDiagnostics(health);
575
- input.onEvent?.({ type: "live-session.diagnostics", data: diagnostics });
576
- } catch (diagError) {
577
- logInternalError("live-session.diagnostics", diagError);
578
- }
579
-
580
- updateLiveAgentStatus(`${input.manifest.runId}:${input.task.id}`, "failed");
581
- return { available: true, exitCode: 1, stdout: stdout.trim(), stderr: message, jsonEvents, error: message };
582
- } finally {
583
- // H6: Unsubscribe listeners FIRST before clearing timer to prevent race
584
- unsubscribe?.();
585
- unsubscribeControlRealtime?.();
586
- if (controlTimer) clearInterval(controlTimer);
587
-
588
- // Phase 8: Emit final health snapshot
589
- try {
590
- const agents = listLiveAgents();
591
- if (agents.length > 0) {
592
- const health = collectLiveSessionHealth(agents, () => undefined);
593
- input.onEvent?.({ type: "live-session.health", data: health });
594
- }
595
- } catch (healthError) {
596
- logInternalError("live-session.health-snapshot", healthError);
597
- }
598
- }
599
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { AgentConfig } from "../agents/agent-config.ts";
4
+ import type { CrewRuntimeConfig } from "../config/config.ts";
5
+ import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
6
+ import { appendEvent } from "../state/event-log.ts";
7
+ import { buildMemoryBlock } from "./agent-memory.ts";
8
+ import { trackTaskUsage } from "./usage-tracker.ts";
9
+ import { createStreamingOutput, type StreamingOutputHandle } from "./streaming-output.ts";
10
+ import { registerLiveAgent, disposeLiveAgentSession, terminateLiveAgent, updateLiveAgentStatus, trackLiveAgentToolStart, trackLiveAgentToolEnd, trackLiveAgentTurnEnd, trackLiveAgentResponseText, markLiveAgentCompleted } from "./live-agent-manager.ts";
11
+ import { applyLiveAgentControlRequest, applyLiveAgentControlRequests, type LiveAgentControlCursor } from "./live-agent-control.ts";
12
+ import { subscribeLiveControlRealtime } from "./live-control-realtime.ts";
13
+ import { eventToSidechainType, sidechainOutputPath, writeSidechainEntry } from "./sidechain-output.ts";
14
+ import type { WorkflowStep } from "../workflows/workflow-config.ts";
15
+ import { isLiveSessionRuntimeAvailable } from "./runtime-resolver.ts";
16
+ import { redactSecrets } from "../utils/redaction.ts";
17
+ import { buildConfiguredModelRouting } from "./model-fallback.ts";
18
+ import { DEFAULT_LIVE_SESSION } from "../config/defaults.ts";
19
+ import { buildYieldReminder, hasYieldInOutput, isYieldEvent, extractYieldResult, validateYieldData, DEFAULT_YIELD_CONFIG, type YieldResult } from "./yield-handler.ts";
20
+ import { buildMcpProxyFromSession } from "./mcp-proxy.ts";
21
+ import { createSubmitResultTool } from "./custom-tools/submit-result-tool.ts";
22
+ import { createIrcTool } from "./custom-tools/irc-tool.ts";
23
+ import { buildExtensionBridge } from "./live-extension-bridge.ts";
24
+ import { logInternalError } from "../utils/internal-error.ts";
25
+ // prose-compressor imported for custom tool descriptions below;
26
+ // tool description compression for SDK-managed tools awaits SDK support.
27
+ import { compressToolDescription } from "./prose-compressor.ts";
28
+ import { buildSensitivePathConstraint } from "./sensitive-paths.ts";
29
+ import { collectLiveSessionHealth, formatLiveSessionDiagnostics, type LiveSessionHealth } from "./live-session-health.ts";
30
+ import { listLiveAgents } from "./live-agent-manager.ts";
31
+
32
+ export interface LiveSessionSpawnInput {
33
+ manifest: TeamRunManifest;
34
+ task: TeamTaskState;
35
+ step: WorkflowStep;
36
+ agent: AgentConfig;
37
+ prompt: string;
38
+ signal?: AbortSignal;
39
+ transcriptPath?: string;
40
+ onEvent?: (event: unknown) => void;
41
+ onOutput?: (text: string) => void;
42
+ runtimeConfig?: CrewRuntimeConfig;
43
+ parentContext?: string;
44
+ parentModel?: unknown;
45
+ modelRegistry?: unknown;
46
+ modelOverride?: string;
47
+ teamRoleModel?: string;
48
+ isCurrent?: () => boolean;
49
+ /** Workspace where this run was initiated — used for session-scoped live-agent visibility. */
50
+ workspaceId: string;
51
+ /** Phase 2: Output schema for validating yield data. */
52
+ outputSchema?: unknown;
53
+ }
54
+
55
+ export interface LiveSessionRunResult {
56
+ available: true;
57
+ exitCode: number | null;
58
+ stdout: string;
59
+ stderr: string;
60
+ jsonEvents: number;
61
+ usage?: UsageState;
62
+ error?: string;
63
+ /** Phase 1: Extracted yield result from submit_result tool call. */
64
+ yieldResult?: YieldResult;
65
+ }
66
+
67
+ export interface LiveSessionUnavailableResult {
68
+ available: false;
69
+ reason: string;
70
+ }
71
+
72
+ export interface LiveSessionPlannedResult {
73
+ available: true;
74
+ reason: string;
75
+ }
76
+
77
+ type LiveSessionModule = Record<string, unknown> & {
78
+ createAgentSession?: (options?: Record<string, unknown>) => Promise<{ session: LiveSessionLike; modelFallbackMessage?: string }>;
79
+ DefaultResourceLoader?: new (options: Record<string, unknown>) => { reload?: () => Promise<void> };
80
+ SessionManager?: { inMemory?: (cwd?: string) => unknown; create?: (cwd?: string, sessionDir?: string) => unknown };
81
+ SettingsManager?: { create?: (cwd?: string, agentDir?: string) => unknown };
82
+ getAgentDir?: () => string;
83
+ };
84
+
85
+ type LiveSessionLike = {
86
+ subscribe?: (listener: (event: unknown) => void) => (() => void);
87
+ prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>;
88
+ steer?: (text: string) => Promise<void>;
89
+ abort?: () => Promise<void> | void;
90
+ dispose?: () => void;
91
+ getStats?: () => unknown;
92
+ stats?: unknown;
93
+ bindExtensions?: (bindings?: Record<string, unknown>) => Promise<void>;
94
+ getActiveToolNames?: () => string[];
95
+ setActiveToolsByName?: (names: string[]) => void;
96
+ };
97
+
98
+ function appendTranscript(filePath: string | undefined, event: unknown): void {
99
+ if (!filePath) return;
100
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
101
+ fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets(event))}\n`, "utf-8");
102
+ }
103
+
104
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
105
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
106
+ }
107
+
108
+ function textFromContent(content: unknown): string[] {
109
+ if (typeof content === "string") return [content];
110
+ if (!Array.isArray(content)) return [];
111
+ return content.flatMap((part) => {
112
+ const obj = asRecord(part);
113
+ if (!obj) return [];
114
+ if (obj.type === "text" && typeof obj.text === "string") return [obj.text];
115
+ if (typeof obj.content === "string") return [obj.content];
116
+ return [];
117
+ });
118
+ }
119
+
120
+ function eventText(event: unknown): string[] {
121
+ const obj = asRecord(event);
122
+ if (!obj) return [];
123
+ const text: string[] = [];
124
+ if (typeof obj.text === "string") text.push(obj.text);
125
+ text.push(...textFromContent(obj.content));
126
+ const message = asRecord(obj.message);
127
+ if (message) text.push(...textFromContent(message.content));
128
+ return text.filter((entry) => entry.trim());
129
+ }
130
+
131
+ function finalAssistantText(event: unknown): string[] {
132
+ const obj = asRecord(event);
133
+ if (!obj || obj.type !== "message_end") return [];
134
+ const message = asRecord(obj.message);
135
+ if (message?.role !== "assistant") return [];
136
+ return textFromContent(message.content);
137
+ }
138
+
139
+ function numberField(obj: Record<string, unknown> | undefined, keys: string[]): number | undefined {
140
+ if (!obj) return undefined;
141
+ for (const key of keys) {
142
+ const value = obj[key];
143
+ if (typeof value === "number" && Number.isFinite(value)) return value;
144
+ }
145
+ return undefined;
146
+ }
147
+
148
+ function modelFromRegistry(modelRegistry: unknown, modelId: string | undefined): unknown {
149
+ if (!modelId || !modelId.includes("/")) return undefined;
150
+ const registry = asRecord(modelRegistry);
151
+ const find = registry?.find;
152
+ if (typeof find !== "function") return undefined;
153
+ const [provider, ...modelParts] = modelId.split("/");
154
+ const id = modelParts.join("/");
155
+ try {
156
+ return find.call(modelRegistry, provider, id);
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ /** Communication intensity by role (caveman-inspired token optimization) */
163
+ const ROLE_INTENSITY: Record<string, "lite" | "full" | "ultra"> = {
164
+ explorer: "ultra",
165
+ analyst: "full",
166
+ planner: "full",
167
+ critic: "full",
168
+ executor: "full",
169
+ reviewer: "full",
170
+ "security-reviewer": "full",
171
+ "test-engineer": "full",
172
+ verifier: "full",
173
+ writer: "lite",
174
+ };
175
+
176
+ function buildCommunicationStyle(role: string): string {
177
+ const intensity = ROLE_INTENSITY[role] ?? "full";
178
+ if (intensity === "lite") return "## Communication\nProfessional concise. No filler/hedging. Full sentences OK.";
179
+ if (intensity === "ultra") return [
180
+ "## Communication (ultra-compressed)",
181
+ "Drop: articles, filler, hedging, pleasantries. Fragments OK.",
182
+ "Pattern: [thing] [action] [reason].",
183
+ "Code/paths/symbols: exact, never abbreviated. Errors quoted exact.",
184
+ "Abbreviate prose words: DB/auth/config/req/res/fn/impl.",
185
+ "Arrows for causality: X → Y. One word when one word enough.",
186
+ "Security/destructive: write normal English. Resume compressed after.",
187
+ ].join("\n");
188
+ return [
189
+ "## Communication (compressed)",
190
+ "Drop: articles (a/an/the), filler (just/really/basically/actually/simply), hedging, pleasantries.",
191
+ "Short synonyms. Fragments OK. Pattern: [thing] [action] [reason]. [next step].",
192
+ "Code/paths/symbols: exact. Errors quoted exact.",
193
+ "Security/destructive: write normal English. Resume compressed after.",
194
+ ].join("\n");
195
+ }
196
+
197
+ function buildOutputContract(role: string): string {
198
+ if (role === "explorer") return [
199
+ "## Output Contract",
200
+ "<path>:<line> `<symbol>` — <≤6 word note>",
201
+ "Group: Defs: / Refs: / Callers: / Tests: / Sites:",
202
+ "Zero hits \"No match.\"",
203
+ "Last line totals: N defs, M refs.",
204
+ ].join("\n");
205
+ if (role === "executor") return [
206
+ "## Output Contract",
207
+ "<path>:<line-range> <change ≤10 words>.",
208
+ "verified: <re-read OK | mismatch @ path:line>.",
209
+ "Refusal tokens: too-big. / needs-confirm. / ambiguous. / regressed.",
210
+ ].join("\n");
211
+ if (role === "reviewer" || role === "security-reviewer") return [
212
+ "## Output Contract",
213
+ "<path>:<line>: <emoji> <severity>: <problem>. <fix>.",
214
+ "Severity: 🔴 bug, 🟡 risk, 🔵 nit, ❓ question.",
215
+ "Zero findings \"No issues.\"",
216
+ "Sorted: file order ascending line numbers.",
217
+ ].join("\n");
218
+ if (role === "verifier") return [
219
+ "## Output Contract",
220
+ "PASS: <what verified> — <evidence ≤20 words>.",
221
+ "FAIL: <what failed> — <reason>. <expected vs actual>.",
222
+ "Evidence: file paths, test output, or diffs.",
223
+ ].join("\n");
224
+ if (role === "writer") return "## Output Contract\nWrite clear documentation. Full sentences. No compression.";
225
+ return ""; // planner, critic, analyst, test-engineer: no strict format
226
+ }
227
+
228
+ /**
229
+ * Phase 3 (caveman): Compress tool descriptions in a live session to reduce
230
+ * input token cost per tool call. MCP tools often have verbose descriptions
231
+ * (e.g. "This tool allows you to search for files in the filesystem..." "Search files in filesystem.").
232
+ * Compresses only description text, never modifies tool names or parameters.
233
+ */
234
+ function compressSessionToolDescriptions(session: LiveSessionLike): void {
235
+ if (typeof session.getActiveToolNames !== "function") return;
236
+ // The Pi SDK doesn't expose a setDescription API, but we can attempt
237
+ // to compress via setActiveToolsByName if the session supports it.
238
+ // For now, this is a no-op that documents the intent for future SDK support.
239
+ // When Pi SDK adds tool description mutation, this function will compress.
240
+ // Side benefit: the import of compressToolDescription ensures the module
241
+ // is loaded and tree-shakeable, so adding the actual logic later is trivial.
242
+ }
243
+
244
+ function liveSystemPrompt(input: LiveSessionSpawnInput): string {
245
+ const memory = input.agent.memory ? buildMemoryBlock(input.agent.name, input.agent.memory, input.task.cwd, Boolean(input.agent.tools?.some((tool) => tool === "write" || tool === "edit"))) : "";
246
+ const role = input.task.role;
247
+ const styleBlock = buildCommunicationStyle(role);
248
+ const contractBlock = buildOutputContract(role);
249
+ const sensitiveConstraint = buildSensitivePathConstraint();
250
+ return [
251
+ "# pi-crew Live Subagent",
252
+ `Run ID: ${input.manifest.runId}`,
253
+ `Task ID: ${input.task.id}`,
254
+ `Role: ${role}`,
255
+ `Agent: ${input.agent.name}`,
256
+ `Working directory: ${input.task.cwd}`,
257
+ "",
258
+ styleBlock,
259
+ contractBlock,
260
+ sensitiveConstraint,
261
+ "",
262
+ input.agent.systemPrompt || "Follow the user task exactly and report verification evidence.",
263
+ memory ? `\n${memory}` : "",
264
+ ].filter(Boolean).join("\n");
265
+ }
266
+
267
+ function filterActiveTools(session: LiveSessionLike, agent: AgentConfig): void {
268
+ if (typeof session.getActiveToolNames !== "function" || typeof session.setActiveToolsByName !== "function") return;
269
+ const recursiveTools = new Set(["team", "Team", "Agent", "get_subagent_result", "steer_subagent"]);
270
+ const allowed = agent.tools?.length ? new Set(agent.tools) : undefined;
271
+ const active = session.getActiveToolNames().filter((name) => !recursiveTools.has(name) && (!allowed || allowed.has(name)));
272
+ session.setActiveToolsByName(active);
273
+ }
274
+
275
+ function usageFromStats(stats: unknown): UsageState | undefined {
276
+ const obj = asRecord(stats);
277
+ if (!obj) return undefined;
278
+ const input = numberField(obj, ["input", "inputTokens", "input_tokens"]);
279
+ const output = numberField(obj, ["output", "outputTokens", "output_tokens"]);
280
+ const cacheRead = numberField(obj, ["cacheRead", "cache_read"]);
281
+ const cacheWrite = numberField(obj, ["cacheWrite", "cache_write"]);
282
+ const cost = numberField(obj, ["cost"]);
283
+ const turns = numberField(obj, ["turns", "turnCount", "turn_count"]);
284
+ return [input, output, cacheRead, cacheWrite, cost, turns].some((value) => value !== undefined) ? { input, output, cacheRead, cacheWrite, cost, turns } : undefined;
285
+ }
286
+
287
+ async function promptWithTimeout(session: LiveSessionLike, text: string, timeoutMs: number, label: string): Promise<boolean> {
288
+ const promptPromise = session.prompt?.(text, { source: "api", expandPromptTemplates: false });
289
+ if (!promptPromise) return false;
290
+ let timer: ReturnType<typeof setTimeout> | undefined;
291
+ try {
292
+ await Promise.race([
293
+ promptPromise,
294
+ new Promise<void>((_, reject) => {
295
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
296
+ timer.unref?.();
297
+ }),
298
+ ]);
299
+ return true;
300
+ } finally {
301
+ if (timer) clearTimeout(timer);
302
+ }
303
+ }
304
+
305
+ export async function probeLiveSessionRuntime(): Promise<LiveSessionUnavailableResult | LiveSessionPlannedResult> {
306
+ const availability = await isLiveSessionRuntimeAvailable();
307
+ if (!availability.available) return { available: false, reason: availability.reason ?? "Live-session runtime is unavailable." };
308
+ return { available: true, reason: "Live-session SDK exports are available. pi-crew can run in-process live agents when runtime.mode=live-session." };
309
+ }
310
+
311
+ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<LiveSessionRunResult> {
312
+ const isCurrent = input.isCurrent ?? (() => true);
313
+ let streamOut: StreamingOutputHandle | undefined;
314
+
315
+ // G1: Capture yield result from custom tool callback
316
+ let customToolYieldResult: YieldResult | undefined;
317
+ let customToolYieldResolved = false;
318
+ if (process.env.PI_CREW_MOCK_LIVE_SESSION === "success") {
319
+ const agentId = `${input.manifest.runId}:${input.task.id}`;
320
+ const inherited = input.runtimeConfig?.inheritContext === true && input.parentContext ? ` with inherited context: ${input.parentContext}` : "";
321
+ const event = { type: "message_end", message: { role: "assistant", content: [{ type: "text", text: `Mock live-session success for ${input.agent.name}${inherited}` }] } };
322
+ const mockSession = { steer: async () => {}, prompt: async () => {}, abort: async () => {} };
323
+ registerLiveAgent({ agentId, runId: input.manifest.runId, taskId: input.task.id, role: input.task.role, agent: input.agent?.name ?? "mock", description: "mock", session: mockSession, status: "running", workspaceId: input.workspaceId }, appendEvent, input.manifest.eventsPath);
324
+ appendTranscript(input.transcriptPath, event);
325
+ const sidechainPath = sidechainOutputPath(input.manifest.stateRoot, input.task.id);
326
+ writeSidechainEntry(sidechainPath, { agentId, type: "user", message: { role: "user", content: input.prompt }, cwd: input.task.cwd });
327
+ writeSidechainEntry(sidechainPath, { agentId, type: "message", message: event, cwd: input.task.cwd });
328
+ if (isCurrent()) input.onEvent?.(event);
329
+ const stdout = `Mock live-session success for ${input.agent.name}${inherited}`;
330
+ if (isCurrent()) input.onOutput?.(stdout);
331
+ updateLiveAgentStatus(agentId, "completed");
332
+ markLiveAgentCompleted(agentId);
333
+ return { available: true, exitCode: 0, stdout, stderr: "", jsonEvents: 1 };
334
+ }
335
+ const availability = await isLiveSessionRuntimeAvailable();
336
+ if (!availability.available) return { available: true, exitCode: 1, stdout: "", stderr: availability.reason ?? "Live-session runtime unavailable.", jsonEvents: 0, error: availability.reason };
337
+ // LAZY: optional peer dependency only loaded when live-session runtime is chosen.
338
+ const mod = await import("@mariozechner/pi-coding-agent") as LiveSessionModule;
339
+ if (typeof mod.createAgentSession !== "function") return { available: true, exitCode: 1, stdout: "", stderr: "createAgentSession export is unavailable.", jsonEvents: 0, error: "createAgentSession export is unavailable." };
340
+ let session: LiveSessionLike | undefined;
341
+ let unsubscribe: (() => void) | undefined;
342
+ let unsubscribeControlRealtime: (() => void) | undefined;
343
+ let controlTimer: ReturnType<typeof setInterval> | undefined;
344
+ let stdout = "";
345
+ let jsonEvents = 0;
346
+ const collectedJsonEvents: Record<string, unknown>[] = [];
347
+ const maxCollectedJsonEvents = 1000;
348
+ let yieldResult: YieldResult | undefined;
349
+
350
+ const agentId = `${input.manifest.runId}:${input.task.id}`;
351
+
352
+ try {
353
+ const agentDir = typeof mod.getAgentDir === "function" ? mod.getAgentDir() : undefined;
354
+ let resourceLoader: unknown;
355
+ if (mod.DefaultResourceLoader && agentDir) {
356
+ resourceLoader = new mod.DefaultResourceLoader({
357
+ cwd: input.task.cwd,
358
+ agentDir,
359
+ noPromptTemplates: true,
360
+ noThemes: true,
361
+ noContextFiles: input.runtimeConfig?.inheritContext !== true,
362
+ systemPromptOverride: () => liveSystemPrompt(input),
363
+ appendSystemPromptOverride: () => [],
364
+ });
365
+ await (resourceLoader as { reload?: () => Promise<void> }).reload?.();
366
+ }
367
+ const modelRouting = buildConfiguredModelRouting({ overrideModel: input.modelOverride, stepModel: input.step.model, teamRoleModel: input.teamRoleModel, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: input.manifest.cwd });
368
+ const resolvedModel = modelFromRegistry(input.modelRegistry, modelRouting.candidates[0] ?? modelRouting.requested) ?? input.parentModel;
369
+ // Phase 4: MCP proxy — will be determined after session creation
370
+ // (we check parent's MCP tools and share connections when available)
371
+ const mcpProxy = buildMcpProxyFromSession([], { shareMcp: true });
372
+
373
+ // G1: Build custom tools (submit_result + irc)
374
+ const submitResultTool = createSubmitResultTool((result) => {
375
+ customToolYieldResult = result;
376
+ customToolYieldResolved = true;
377
+ });
378
+ const ircTool = createIrcTool(agentId);
379
+ const customTools = [submitResultTool, ircTool];
380
+
381
+ const sessionCreateStart = Date.now();
382
+ const created = await mod.createAgentSession({
383
+ cwd: input.task.cwd,
384
+ ...(agentDir ? { agentDir } : {}),
385
+ ...(resourceLoader ? { resourceLoader } : {}),
386
+ ...(mod.SessionManager?.inMemory ? { sessionManager: mod.SessionManager.inMemory(input.task.cwd) } : {}),
387
+ ...(mod.SettingsManager?.create && agentDir ? { settingsManager: mod.SettingsManager.create(input.task.cwd, agentDir) } : {}),
388
+ ...(input.modelRegistry ? { modelRegistry: input.modelRegistry } : {}),
389
+ ...(resolvedModel ? { model: resolvedModel } : {}),
390
+ ...(input.agent.thinking ? { thinkingLevel: input.agent.thinking } : {}),
391
+ ...(mcpProxy.enableMcp ? {} : { enableMCP: false }),
392
+ customTools,
393
+ });
394
+ session = created.session;
395
+ appendEvent(input.manifest.eventsPath, { type: "live-session.session_created", runId: input.manifest.runId, taskId: input.task.id, data: { elapsedMs: Date.now() - sessionCreateStart, modelFallbackMessage: created.modelFallbackMessage } });
396
+ filterActiveTools(session, input.agent);
397
+
398
+ // Diagnostic: log before bindExtensions so we can identify extension-loading hangs
399
+ const bindExtensionsStart = Date.now();
400
+ try {
401
+ await Promise.race([
402
+ session.bindExtensions?.({}) ?? Promise.resolve(),
403
+ new Promise<void>((_, reject) => setTimeout(() => reject(new Error("bindExtensions timed out after 30s")), 30_000)),
404
+ ]);
405
+ } catch (bindError) {
406
+ const msg = bindError instanceof Error ? bindError.message : String(bindError);
407
+ appendEvent(input.manifest.eventsPath, { type: "live-session.bind_extensions_error", runId: input.manifest.runId, taskId: input.task.id, data: { elapsedMs: Date.now() - bindExtensionsStart, error: msg } });
408
+ // Continue without extensions — they should not block the session
409
+ }
410
+
411
+ // Phase 3 (caveman): Compress tool descriptions to reduce input token cost
412
+ compressSessionToolDescriptions(session);
413
+
414
+ // Phase 5: Initialize extension runner bridge if available
415
+ // The bridge provides extension-like APIs (sendMessage, setActiveTools, etc.)
416
+ // to the extension runner if the session exposes one.
417
+ const extensionBridge = buildExtensionBridge(session as never);
418
+ if (extensionBridge) {
419
+ const extRunner = (session as Record<string, unknown>).extensionRunner;
420
+ if (extRunner && typeof (extRunner as Record<string, unknown>).initialize === "function") {
421
+ try {
422
+ (extRunner as { initialize: (apis: unknown, host: unknown) => void }).initialize(extensionBridge.apis, extensionBridge.host);
423
+ if (typeof (extRunner as Record<string, unknown>).emit === "function") {
424
+ await (extRunner as { emit: (event: unknown) => Promise<void> }).emit({ type: "session_start" });
425
+ }
426
+ } catch {
427
+ // Extension runner initialization failure should not block the session
428
+ }
429
+ }
430
+ }
431
+
432
+ registerLiveAgent({ agentId, runId: input.manifest.runId, taskId: input.task.id, role: input.task.role, agent: input.agent?.name ?? "unknown", description: input.task.adaptive?.task ?? input.step?.task ?? "", modelName: (resolvedModel as { name?: string })?.name, session, status: "running", workspaceId: input.workspaceId }, appendEvent, input.manifest.eventsPath);
433
+ streamOut = createStreamingOutput(input.manifest, input.task.id);
434
+ let controlCursor: LiveAgentControlCursor = { offset: 0 };
435
+ const seenControlRequestIds = new Set<string>();
436
+ let controlBusy = false;
437
+ const pollControl = async () => {
438
+ if (!isCurrent() || controlBusy || !session) return;
439
+ controlBusy = true;
440
+ try {
441
+ controlCursor = await applyLiveAgentControlRequests({ manifest: input.manifest, taskId: input.task.id, agentId, session, cursor: controlCursor, seenRequestIds: seenControlRequestIds });
442
+ } finally {
443
+ controlBusy = false;
444
+ }
445
+ };
446
+ unsubscribeControlRealtime = subscribeLiveControlRealtime((request) => {
447
+ if (!isCurrent() || request.runId !== input.manifest.runId || request.taskId !== input.task.id || !session) return;
448
+ void applyLiveAgentControlRequest({ request, taskId: input.task.id, agentId, session, seenRequestIds: seenControlRequestIds });
449
+ });
450
+ await pollControl();
451
+ controlTimer = setInterval(() => {
452
+ if (isCurrent()) void pollControl();
453
+ }, 500);
454
+ let turnCount = 0;
455
+ let softLimitReached = false;
456
+ const maxTurns = input.runtimeConfig?.maxTurns;
457
+ const graceTurns = input.runtimeConfig?.graceTurns ?? 5;
458
+ const sidechainPath = sidechainOutputPath(input.manifest.stateRoot, input.task.id);
459
+ writeSidechainEntry(sidechainPath, { agentId, type: "user", message: { role: "user", content: input.prompt }, cwd: input.task.cwd });
460
+ if (typeof session.subscribe === "function") {
461
+ unsubscribe = session.subscribe((event) => {
462
+ if (!isCurrent()) return;
463
+ jsonEvents += 1;
464
+ appendTranscript(input.transcriptPath, event);
465
+ const sidechainType = eventToSidechainType(event);
466
+ if (sidechainType) writeSidechainEntry(sidechainPath, { agentId, type: sidechainType, message: event, cwd: input.task.cwd });
467
+ const obj = asRecord(event);
468
+ if (obj?.type === "turn_end") {
469
+ turnCount += 1;
470
+ trackLiveAgentTurnEnd(agentId);
471
+ if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
472
+ softLimitReached = true;
473
+ void session?.steer?.("You have reached your turn limit. Wrap up immediately provide your final answer now.");
474
+ } else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
475
+ void session?.abort?.();
476
+ }
477
+ }
478
+ // Accumulate lifetime usage that survives compaction
479
+ if (obj?.type === "message_end" && (obj as any).message?.role === "assistant") {
480
+ const u = (obj as any).message?.usage;
481
+ if (u) {
482
+ trackTaskUsage(input.task.id, {
483
+ input: typeof u.input === "number" ? u.input : 0,
484
+ output: typeof u.output === "number" ? u.output : 0,
485
+ cacheWrite: typeof u.cacheWrite === "number" ? u.cacheWrite : 0,
486
+ });
487
+ }
488
+ }
489
+ input.onEvent?.(event);
490
+ const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
491
+ if (text.trim()) {
492
+ stdout += `${text}\n`;
493
+ streamOut?.write(text + "\n");
494
+ trackLiveAgentResponseText(agentId, text);
495
+ input.onOutput?.(text);
496
+ }
497
+ // G2: Track tool start/end for activity display
498
+ if (obj?.type === "tool_use" || obj?.type === "tool_execution_start") {
499
+ const toolName = (obj as any).tool?.name ?? (obj as any).toolName ?? (obj as any).name ?? "unknown";
500
+ trackLiveAgentToolStart(agentId, toolName);
501
+ }
502
+ if (obj?.type === "tool_result" || obj?.type === "tool_execution_end") {
503
+ const toolName = (obj as any).tool?.name ?? (obj as any).toolName ?? (obj as any).name ?? "unknown";
504
+ trackLiveAgentToolEnd(agentId, toolName);
505
+ }
506
+ // Phase 1: collect events for yield detection
507
+ if (event && typeof event === "object" && !Array.isArray(event)) {
508
+ collectedJsonEvents.push(event as Record<string, unknown>);
509
+ if (collectedJsonEvents.length > maxCollectedJsonEvents) collectedJsonEvents.splice(0, collectedJsonEvents.length - maxCollectedJsonEvents);
510
+ }
511
+ });
512
+ }
513
+ if (input.signal) {
514
+ if (input.signal.aborted) await session.abort?.();
515
+ else input.signal.addEventListener("abort", () => { void session?.abort?.(); }, { once: true });
516
+ }
517
+ const effectivePrompt = input.runtimeConfig?.inheritContext === true && input.parentContext ? `${input.parentContext}\n\n---\n# Live Subagent Task\n${input.prompt}` : input.prompt;
518
+
519
+ // Diagnostic: log prompt size and timing
520
+ const promptStart = Date.now();
521
+ appendEvent(input.manifest.eventsPath, { type: "live-session.prompt_start", runId: input.manifest.runId, taskId: input.task.id, data: { promptLength: effectivePrompt.length, agent: input.agent.name, role: input.task.role } });
522
+
523
+ // Phase 3: Wrap session.prompt with timeout for graceful cancellation
524
+ const sessionTimeoutMs = DEFAULT_LIVE_SESSION.responseTimeoutMs;
525
+ try {
526
+ await promptWithTimeout(session, effectivePrompt, sessionTimeoutMs, "Live-session");
527
+ } catch (promptError) {
528
+ const msg = promptError instanceof Error ? promptError.message : String(promptError);
529
+ appendEvent(input.manifest.eventsPath, { type: "live-session.prompt_error", runId: input.manifest.runId, taskId: input.task.id, data: { elapsedMs: Date.now() - promptStart, error: msg } });
530
+ if (msg.includes("timed out")) {
531
+ await session.abort?.();
532
+ updateLiveAgentStatus(agentId, "failed");
533
+ return { available: true, exitCode: 1, stdout: stdout.trim(), stderr: msg, jsonEvents, error: msg };
534
+ }
535
+ throw promptError;
536
+ }
537
+ appendEvent(input.manifest.eventsPath, { type: "live-session.prompt_done", runId: input.manifest.runId, taskId: input.task.id, data: { elapsedMs: Date.now() - promptStart, jsonEvents, outputLength: stdout.length } });
538
+
539
+ // --- Phase 1: Yield enforcement loop ---
540
+ // After the initial prompt completes, check if the worker called submit_result.
541
+ // Priority: 1) custom tool callback (G1), 2) JSON event detection (legacy).
542
+ const yieldConfig = input.runtimeConfig?.yield ?? { enabled: DEFAULT_YIELD_CONFIG.enabled };
543
+ const yieldEnabled = yieldConfig.enabled !== false;
544
+ if (yieldEnabled && session) {
545
+ // Check custom tool callback first (G1)
546
+ if (customToolYieldResolved && customToolYieldResult) {
547
+ yieldResult = customToolYieldResult;
548
+ } else {
549
+ // Legacy: detect from JSON events
550
+ const alreadyYielded = hasYieldInOutput(collectedJsonEvents);
551
+ if (alreadyYielded) {
552
+ const yieldEvent = collectedJsonEvents.find((e) => isYieldEvent(e));
553
+ if (yieldEvent) yieldResult = extractYieldResult(yieldEvent);
554
+ }
555
+ }
556
+ // Phase 2: Validate yield data against output schema if provided
557
+ let schemaFailures = 0;
558
+ const maxSchemaFailures = 2;
559
+ if (yieldResult && input.outputSchema) {
560
+ const validation = await validateYieldData(yieldResult.structuredData, input.outputSchema);
561
+ if (!validation.valid) {
562
+ schemaFailures++;
563
+ yieldResult = undefined;
564
+ customToolYieldResolved = false;
565
+ const schemaReminder = `Your submit_result data did not match the required schema: ${validation.error}. Please fix and call submit_result again with valid data.`;
566
+ try {
567
+ await promptWithTimeout(session, schemaReminder, Math.min(sessionTimeoutMs, DEFAULT_LIVE_SESSION.idleWaitTimeoutMs), "Live-session schema reminder");
568
+ } catch {
569
+ /* ignore */
570
+ }
571
+ await new Promise((resolve) => setTimeout(resolve, DEFAULT_LIVE_SESSION.yieldPollIntervalMs));
572
+ // Check again after schema reminder
573
+ if (customToolYieldResolved && customToolYieldResult) {
574
+ yieldResult = customToolYieldResult;
575
+ } else {
576
+ const newEvents = collectedJsonEvents.slice(-10);
577
+ if (hasYieldInOutput(newEvents)) {
578
+ const yieldEvent = newEvents.find((e) => isYieldEvent(e));
579
+ if (yieldEvent) {
580
+ const candidate = extractYieldResult(yieldEvent);
581
+ if (candidate && input.outputSchema) {
582
+ const revalidation = await validateYieldData(candidate.structuredData, input.outputSchema);
583
+ if (revalidation.valid || schemaFailures >= maxSchemaFailures) {
584
+ yieldResult = candidate;
585
+ }
586
+ }
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ // Reminder loop only if yield not yet received
593
+ const maxReminders = yieldConfig.maxReminders ?? DEFAULT_LIVE_SESSION.maxYieldRetries;
594
+ let retryCount = 0;
595
+ while (!customToolYieldResolved && !yieldResult && retryCount < maxReminders && !input.signal?.aborted) {
596
+ retryCount++;
597
+ const reminder = buildYieldReminder(retryCount, maxReminders, yieldConfig.reminderPrompt);
598
+ const prevTools = typeof session.getActiveToolNames === "function" ? session.getActiveToolNames() : [];
599
+ try {
600
+ // G6: Constrain tool set to submit_result before sending reminder
601
+ if (typeof session.setActiveToolsByName === "function" && prevTools.length > 0) {
602
+ session.setActiveToolsByName(["submit_result"]);
603
+ }
604
+ await promptWithTimeout(session, reminder, Math.min(sessionTimeoutMs, DEFAULT_LIVE_SESSION.idleWaitTimeoutMs), "Live-session yield reminder");
605
+ } catch {
606
+ break;
607
+ } finally {
608
+ // Restore previous tools even if reminder prompt times out/throws.
609
+ if (typeof session.setActiveToolsByName === "function" && prevTools.length > 0) {
610
+ session.setActiveToolsByName(prevTools);
611
+ }
612
+ }
613
+ const pollInterval = DEFAULT_LIVE_SESSION.yieldPollIntervalMs;
614
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
615
+ // Check custom tool callback
616
+ if (customToolYieldResolved && customToolYieldResult) {
617
+ yieldResult = customToolYieldResult;
618
+ break;
619
+ }
620
+ // Legacy: check JSON events
621
+ if (hasYieldInOutput(collectedJsonEvents.slice(-10))) {
622
+ const yieldEvent = collectedJsonEvents.slice(-10).find((e) => isYieldEvent(e));
623
+ if (yieldEvent) yieldResult = extractYieldResult(yieldEvent);
624
+ break;
625
+ }
626
+ }
627
+ if (!customToolYieldResolved && !yieldResult && !input.signal?.aborted && retryCount >= maxReminders) {
628
+ input.onEvent?.({ type: "task.attention", runId: input.manifest.runId, taskId: input.task.id, message: "Live-session worker completed without calling submit_result tool.", data: { activityState: "needs_attention", reason: "no_yield", attempts: retryCount } });
629
+ }
630
+ }
631
+
632
+ const usage = usageFromStats(typeof session.getStats === "function" ? session.getStats() : session.stats);
633
+ updateLiveAgentStatus(agentId, "completed");
634
+ markLiveAgentCompleted(agentId);
635
+ return { available: true, exitCode: 0, stdout: stdout.trim(), stderr: created.modelFallbackMessage ?? "", jsonEvents, usage, yieldResult };
636
+ } catch (error) {
637
+ const message = error instanceof Error ? error.message : String(error);
638
+
639
+ // Phase 8: Log diagnostics on failure
640
+ try {
641
+ const agents = listLiveAgents();
642
+ const health = collectLiveSessionHealth(agents, () => undefined);
643
+ const diagnostics = formatLiveSessionDiagnostics(health);
644
+ input.onEvent?.({ type: "live-session.diagnostics", data: diagnostics });
645
+ } catch (diagError) {
646
+ logInternalError("live-session.diagnostics", diagError);
647
+ }
648
+
649
+ updateLiveAgentStatus(`${input.manifest.runId}:${input.task.id}`, "failed");
650
+ return { available: true, exitCode: 1, stdout: stdout.trim(), stderr: message, jsonEvents, error: message };
651
+ } finally {
652
+ // H6: Unsubscribe listeners FIRST before clearing timer to prevent race
653
+ unsubscribe?.();
654
+ unsubscribeControlRealtime?.();
655
+ if (controlTimer) clearInterval(controlTimer);
656
+ streamOut?.close();
657
+ if (input.signal?.aborted) {
658
+ await terminateLiveAgent(agentId, "cancelled", appendEvent, input.manifest.eventsPath);
659
+ } else {
660
+ // Dispose the session to free resources, but keep the handle in the registry
661
+ // for resume/follow-up. Removing the handle entirely breaks steer/followUp/resume.
662
+ disposeLiveAgentSession(agentId);
663
+ }
664
+
665
+ // Phase 8: Emit final health snapshot
666
+ try {
667
+ const agents = listLiveAgents();
668
+ if (agents.length > 0) {
669
+ const health = collectLiveSessionHealth(agents, () => undefined);
670
+ input.onEvent?.({ type: "live-session.health", data: health });
671
+ }
672
+ } catch (healthError) {
673
+ logInternalError("live-session.health-snapshot", healthError);
674
+ }
675
+ }
676
+ }