@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4685 -4614
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +374 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  25. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  26. package/dist/types/live/attestation.d.ts +2 -0
  27. package/dist/types/live/controller.d.ts +48 -0
  28. package/dist/types/live/protocol.d.ts +85 -0
  29. package/dist/types/live/protocol.test.d.ts +1 -0
  30. package/dist/types/live/transport.d.ts +35 -0
  31. package/dist/types/live/visualizer.d.ts +30 -0
  32. package/dist/types/lsp/client.d.ts +16 -1
  33. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  34. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  35. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  36. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  37. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  38. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  39. package/dist/types/modes/components/custom-message.d.ts +1 -1
  40. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  41. package/dist/types/modes/components/message-frame.d.ts +8 -4
  42. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  43. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  44. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  45. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  46. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  48. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  49. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +4 -1
  54. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  55. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  56. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  57. package/dist/types/registry/agent-registry.d.ts +11 -4
  58. package/dist/types/sdk.d.ts +15 -1
  59. package/dist/types/secrets/index.d.ts +15 -2
  60. package/dist/types/secrets/obfuscator.d.ts +113 -15
  61. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  62. package/dist/types/session/agent-session-events.d.ts +84 -0
  63. package/dist/types/session/agent-session-types.d.ts +321 -0
  64. package/dist/types/session/agent-session.d.ts +135 -753
  65. package/dist/types/session/async-job-delivery.d.ts +28 -0
  66. package/dist/types/session/auth-storage.d.ts +1 -1
  67. package/dist/types/session/bash-runner.d.ts +75 -0
  68. package/dist/types/session/blob-store.d.ts +11 -1
  69. package/dist/types/session/blob-store.test.d.ts +1 -0
  70. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  71. package/dist/types/session/eval-runner.d.ts +51 -0
  72. package/dist/types/session/irc-bridge.d.ts +46 -0
  73. package/dist/types/session/messages.d.ts +24 -0
  74. package/dist/types/session/model-controls.d.ts +180 -0
  75. package/dist/types/session/prewalk.d.ts +46 -0
  76. package/dist/types/session/queued-messages.d.ts +22 -0
  77. package/dist/types/session/redis-session-storage.d.ts +1 -0
  78. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  79. package/dist/types/session/role-models.d.ts +15 -0
  80. package/dist/types/session/session-advisors.d.ts +228 -0
  81. package/dist/types/session/session-entries.d.ts +8 -0
  82. package/dist/types/session/session-handoff.d.ts +70 -0
  83. package/dist/types/session/session-history-format.d.ts +6 -1
  84. package/dist/types/session/session-maintenance.d.ts +246 -0
  85. package/dist/types/session/session-manager.d.ts +41 -0
  86. package/dist/types/session/session-memory.d.ts +56 -0
  87. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  88. package/dist/types/session/session-stats.d.ts +48 -0
  89. package/dist/types/session/session-tools.d.ts +158 -0
  90. package/dist/types/session/session-workspace.d.ts +32 -0
  91. package/dist/types/session/stream-guards.d.ts +44 -0
  92. package/dist/types/session/streaming-output.d.ts +8 -0
  93. package/dist/types/session/todo-tracker.d.ts +56 -0
  94. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  95. package/dist/types/session/turn-recovery.d.ts +225 -0
  96. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  97. package/dist/types/stt/index.d.ts +0 -2
  98. package/dist/types/stt/stt-controller.d.ts +7 -0
  99. package/dist/types/system-prompt.d.ts +2 -0
  100. package/dist/types/task/executor.d.ts +4 -1
  101. package/dist/types/task/output-manager.d.ts +3 -1
  102. package/dist/types/tiny/title-client.d.ts +10 -0
  103. package/dist/types/tools/approval.d.ts +1 -0
  104. package/dist/types/tools/browser/launch.d.ts +5 -0
  105. package/dist/types/tools/builtin-names.d.ts +1 -1
  106. package/dist/types/tools/computer/protocol.d.ts +43 -0
  107. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  108. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  109. package/dist/types/tools/computer/worker.d.ts +15 -0
  110. package/dist/types/tools/computer-renderer.d.ts +22 -0
  111. package/dist/types/tools/computer.d.ts +71 -0
  112. package/dist/types/tools/context.d.ts +2 -0
  113. package/dist/types/tools/default-renderer.d.ts +21 -0
  114. package/dist/types/tools/image-gen.d.ts +4 -4
  115. package/dist/types/tools/image-providers.d.ts +38 -0
  116. package/dist/types/tools/index.d.ts +7 -0
  117. package/dist/types/tools/path-utils.d.ts +8 -0
  118. package/dist/types/tools/todo.d.ts +10 -4
  119. package/dist/types/tools/xdev.d.ts +5 -1
  120. package/dist/types/tts/streaming-player.d.ts +10 -29
  121. package/dist/types/tts/vocalizer.d.ts +5 -0
  122. package/dist/types/utils/jj.d.ts +29 -0
  123. package/dist/types/utils/lang-from-path.d.ts +1 -0
  124. package/dist/types/utils/title-generator.d.ts +30 -0
  125. package/dist/types/utils/tools-manager.d.ts +1 -2
  126. package/dist/types/vibe/runtime.d.ts +36 -8
  127. package/dist/types/web/search/index.d.ts +1 -1
  128. package/dist/types/web/search/provider.d.ts +15 -5
  129. package/dist/types/web/search/types.d.ts +94 -0
  130. package/package.json +12 -12
  131. package/scripts/generate-share-viewer.ts +4 -6
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +3 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/setup-cli.ts +2 -14
  141. package/src/cli/usage-cli.ts +10 -1
  142. package/src/cli.ts +8 -0
  143. package/src/collab/host.ts +3 -2
  144. package/src/commands/bench.ts +18 -4
  145. package/src/commands/launch.ts +4 -0
  146. package/src/config/model-registry.ts +16 -2
  147. package/src/config/models-config-schema.ts +15 -4
  148. package/src/config/provider-globals.ts +9 -9
  149. package/src/config/service-tier.ts +26 -1
  150. package/src/config/settings-schema.ts +270 -51
  151. package/src/config/settings.ts +81 -2
  152. package/src/dap/client.ts +37 -4
  153. package/src/edit/index.ts +49 -12
  154. package/src/eval/executor-base.ts +1 -0
  155. package/src/eval/js/executor.ts +2 -0
  156. package/src/eval/py/executor.ts +103 -33
  157. package/src/exec/bash-executor.ts +90 -6
  158. package/src/exec/direnv.ts +145 -0
  159. package/src/export/html/args.ts +20 -0
  160. package/src/export/html/index.ts +45 -47
  161. package/src/export/html/template.css +19 -1
  162. package/src/export/html/template.html +6 -0
  163. package/src/export/html/template.js +21 -0
  164. package/src/export/html/tool-views.generated.js +31 -31
  165. package/src/export/html/web-palette.ts +116 -132
  166. package/src/export/share.ts +249 -49
  167. package/src/extensibility/extensions/loader.ts +29 -1
  168. package/src/extensibility/extensions/runner.ts +6 -0
  169. package/src/extensibility/extensions/types.ts +33 -2
  170. package/src/extensibility/extensions/wrapper.ts +68 -12
  171. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  172. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  173. package/src/hindsight/state.ts +64 -6
  174. package/src/internal-urls/local-protocol.ts +2 -1
  175. package/src/internal-urls/memory-protocol.ts +2 -2
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/attestation.ts +91 -0
  180. package/src/live/controller.ts +517 -0
  181. package/src/live/prompts/agent-final-message.md +3 -0
  182. package/src/live/prompts/live-instructions.md +23 -0
  183. package/src/live/protocol.test.ts +140 -0
  184. package/src/live/protocol.ts +233 -0
  185. package/src/live/transport.ts +422 -0
  186. package/src/live/visualizer.ts +214 -0
  187. package/src/lsp/client.ts +29 -9
  188. package/src/lsp/index.ts +19 -4
  189. package/src/main.ts +8 -1
  190. package/src/mcp/config-writer.test.ts +43 -0
  191. package/src/mcp/config-writer.ts +109 -82
  192. package/src/mcp/oauth-discovery.ts +10 -2
  193. package/src/mcp/smithery-auth.test.ts +29 -0
  194. package/src/mcp/smithery-auth.ts +3 -2
  195. package/src/memories/index.ts +40 -20
  196. package/src/memory-backend/tool-names.ts +2 -0
  197. package/src/mnemopi/backend.ts +24 -7
  198. package/src/modes/acp/acp-agent.ts +56 -10
  199. package/src/modes/acp/acp-event-mapper.ts +8 -1
  200. package/src/modes/components/agent-hub.ts +1 -1
  201. package/src/modes/components/assistant-message.ts +7 -2
  202. package/src/modes/components/custom-message.ts +4 -1
  203. package/src/modes/components/login-dialog.ts +14 -4
  204. package/src/modes/components/message-frame.ts +14 -8
  205. package/src/modes/components/plan-review-overlay.ts +350 -35
  206. package/src/modes/components/session-account-selector.ts +62 -0
  207. package/src/modes/components/settings-defs.ts +23 -1
  208. package/src/modes/components/settings-selector.ts +169 -1
  209. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  210. package/src/modes/components/status-line/component.ts +123 -2
  211. package/src/modes/components/tool-execution.ts +28 -112
  212. package/src/modes/controllers/command-controller.ts +56 -15
  213. package/src/modes/controllers/event-controller.ts +111 -16
  214. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  215. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  216. package/src/modes/controllers/input-controller.ts +47 -39
  217. package/src/modes/controllers/live-command-controller.ts +255 -0
  218. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  219. package/src/modes/controllers/omfg-controller.ts +44 -40
  220. package/src/modes/controllers/selector-controller.ts +80 -10
  221. package/src/modes/controllers/session-focus-controller.ts +6 -1
  222. package/src/modes/interactive-mode.ts +176 -38
  223. package/src/modes/runtime-init.ts +2 -0
  224. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  225. package/src/modes/theme/theme.ts +1 -1
  226. package/src/modes/types.ts +4 -1
  227. package/src/modes/utils/context-usage.ts +23 -7
  228. package/src/modes/utils/ui-helpers.ts +10 -5
  229. package/src/plan-mode/plan-files.ts +40 -0
  230. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  231. package/src/prompts/bench/cache-prefix.md +3 -0
  232. package/src/prompts/bench/cache-suffix.md +1 -0
  233. package/src/prompts/system/computer-safety.md +14 -0
  234. package/src/prompts/system/custom-system-prompt.md +1 -1
  235. package/src/prompts/system/project-prompt.md +8 -1
  236. package/src/prompts/system/subagent-async-pending.md +6 -0
  237. package/src/prompts/system/system-prompt.md +1 -1
  238. package/src/prompts/system/workflow-notice.md +48 -40
  239. package/src/prompts/system/xdev-mount-notice.md +4 -0
  240. package/src/prompts/tools/computer.md +26 -0
  241. package/src/prompts/tools/todo.md +3 -1
  242. package/src/registry/agent-lifecycle.ts +116 -46
  243. package/src/registry/agent-registry.ts +35 -8
  244. package/src/registry/persisted-agents.ts +26 -2
  245. package/src/sdk.ts +215 -136
  246. package/src/secrets/index.ts +127 -4
  247. package/src/secrets/obfuscator.ts +2328 -105
  248. package/src/session/acp-permission-gate.ts +165 -0
  249. package/src/session/agent-session-error-log.test.ts +1 -1
  250. package/src/session/agent-session-events.ts +66 -0
  251. package/src/session/agent-session-types.ts +343 -0
  252. package/src/session/agent-session.ts +1940 -12218
  253. package/src/session/artifacts.ts +6 -5
  254. package/src/session/async-job-delivery.ts +74 -0
  255. package/src/session/auth-storage.ts +1 -0
  256. package/src/session/bash-runner.ts +326 -0
  257. package/src/session/blob-store.test.ts +56 -0
  258. package/src/session/blob-store.ts +18 -2
  259. package/src/session/checkpoint-entries.ts +81 -0
  260. package/src/session/eval-runner.ts +217 -0
  261. package/src/session/indexed-session-storage.ts +9 -2
  262. package/src/session/irc-bridge.ts +203 -0
  263. package/src/session/messages.ts +266 -1
  264. package/src/session/model-controls.ts +714 -0
  265. package/src/session/prewalk.ts +252 -0
  266. package/src/session/queued-messages.ts +93 -0
  267. package/src/session/redis-session-storage.ts +51 -11
  268. package/src/session/retry-fallback-chains.ts +455 -0
  269. package/src/session/role-models.ts +85 -0
  270. package/src/session/session-advisors.ts +1679 -0
  271. package/src/session/session-context.ts +6 -3
  272. package/src/session/session-entries.ts +8 -0
  273. package/src/session/session-handoff.ts +308 -0
  274. package/src/session/session-history-format.ts +20 -9
  275. package/src/session/session-loader.ts +0 -46
  276. package/src/session/session-maintenance.ts +2983 -0
  277. package/src/session/session-manager.ts +390 -30
  278. package/src/session/session-memory.ts +222 -0
  279. package/src/session/session-provider-boundary.ts +307 -0
  280. package/src/session/session-stats.ts +293 -0
  281. package/src/session/session-tools.ts +980 -0
  282. package/src/session/session-workspace.ts +53 -0
  283. package/src/session/stream-guards.ts +417 -0
  284. package/src/session/streaming-output.ts +52 -5
  285. package/src/session/todo-tracker.ts +380 -0
  286. package/src/session/ttsr-coordinator.ts +496 -0
  287. package/src/session/turn-recovery.ts +1629 -0
  288. package/src/slash-commands/builtin-registry.ts +257 -20
  289. package/src/slash-commands/helpers/session-pin.ts +44 -0
  290. package/src/slash-commands/helpers/usage-report.ts +22 -3
  291. package/src/stt/downloader.ts +0 -2
  292. package/src/stt/index.ts +0 -2
  293. package/src/stt/stt-controller.ts +57 -146
  294. package/src/system-prompt.ts +45 -17
  295. package/src/task/executor.ts +285 -70
  296. package/src/task/index.ts +98 -105
  297. package/src/task/output-manager.ts +25 -3
  298. package/src/task/persisted-revive.ts +4 -3
  299. package/src/task/structured-subagent.ts +1 -0
  300. package/src/tiny/title-client.ts +22 -0
  301. package/src/tools/approval.ts +55 -10
  302. package/src/tools/bash-interactive.ts +116 -86
  303. package/src/tools/bash-skill-urls.ts +28 -2
  304. package/src/tools/bash.ts +339 -123
  305. package/src/tools/browser/launch.ts +9 -1
  306. package/src/tools/browser/tab-supervisor.ts +8 -5
  307. package/src/tools/browser.ts +94 -47
  308. package/src/tools/builtin-names.ts +1 -0
  309. package/src/tools/computer/protocol.ts +28 -0
  310. package/src/tools/computer/supervisor.ts +258 -0
  311. package/src/tools/computer/worker-entry.ts +25 -0
  312. package/src/tools/computer/worker.ts +135 -0
  313. package/src/tools/computer-renderer.ts +108 -0
  314. package/src/tools/computer.ts +433 -0
  315. package/src/tools/context.ts +2 -0
  316. package/src/tools/default-renderer.ts +139 -0
  317. package/src/tools/essential-tools.ts +1 -0
  318. package/src/tools/image-gen.ts +20 -28
  319. package/src/tools/image-providers.ts +50 -0
  320. package/src/tools/index.ts +14 -0
  321. package/src/tools/path-utils.ts +49 -2
  322. package/src/tools/read.ts +156 -88
  323. package/src/tools/renderers.ts +9 -1
  324. package/src/tools/todo.ts +101 -11
  325. package/src/tools/vibe.ts +1 -2
  326. package/src/tools/write.ts +51 -1
  327. package/src/tools/xdev.ts +121 -29
  328. package/src/tts/streaming-player.ts +82 -283
  329. package/src/tts/vocalizer.ts +18 -1
  330. package/src/utils/clipboard.ts +1 -30
  331. package/src/utils/jj.ts +125 -4
  332. package/src/utils/lang-from-path.ts +7 -0
  333. package/src/utils/mac-file-urls.applescript +37 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/utils/tool-choice.ts +14 -0
  336. package/src/utils/tools-manager.ts +1 -19
  337. package/src/vibe/runtime.ts +852 -72
  338. package/src/web/search/index.ts +3 -6
  339. package/src/web/search/provider.ts +31 -16
  340. package/src/web/search/providers/xai.ts +7 -1
  341. package/src/web/search/types.ts +3 -0
  342. package/dist/types/stt/recorder.d.ts +0 -30
  343. package/dist/types/stt/transcriber.d.ts +0 -14
  344. package/dist/types/stt/wav.d.ts +0 -29
  345. package/dist/types/tts/player.d.ts +0 -32
  346. package/src/stt/recorder.ts +0 -551
  347. package/src/stt/transcriber.ts +0 -60
  348. package/src/stt/wav.ts +0 -173
  349. package/src/tts/player.ts +0 -137
@@ -0,0 +1,517 @@
1
+ import * as os from "node:os";
2
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
4
+ import { AudioCapture } from "@oh-my-pi/pi-natives";
5
+ import { prompt } from "@oh-my-pi/pi-utils";
6
+ import type { AgentSession } from "../session/agent-session";
7
+ import type { AgentSessionEvent } from "../session/agent-session-events";
8
+ import { LIVE_DELEGATION_MESSAGE_TYPE } from "../session/messages";
9
+ import agentFinalMessageTemplate from "./prompts/agent-final-message.md" with { type: "text" };
10
+ import liveInstructionsTemplate from "./prompts/live-instructions.md" with { type: "text" };
11
+ import {
12
+ buildDelegationContextAppend,
13
+ buildSessionClose,
14
+ chunkLiveContext,
15
+ type LiveClientMessage,
16
+ type LiveServerEvent,
17
+ } from "./protocol";
18
+ import { CodexLiveTransport } from "./transport";
19
+ import type { LivePhase } from "./visualizer";
20
+
21
+ const DEFAULT_VOICE = "sol";
22
+ const OUTPUT_ACTIVE_LEVEL = 0.015;
23
+ const MIN_BARGE_IN_LEVEL = 0.04;
24
+ const OUTPUT_ECHO_RATIO = 0.65;
25
+
26
+ /** Incremental or final transcript for one realtime conversational turn. */
27
+ export interface LiveTranscript {
28
+ role: "user" | "assistant";
29
+ text: string;
30
+ /** Monotonic role-local turn number used to coalesce streaming updates. */
31
+ turn: number;
32
+ final: boolean;
33
+ }
34
+
35
+ /** UI notifications emitted during a live session. */
36
+ export interface LiveSessionCallbacks {
37
+ /** Reports connection and activity phase changes. */
38
+ onPhase(phase: LivePhase): void;
39
+ /** Reports clamped microphone and speaker RMS levels. */
40
+ onLevels(input: number, output: number): void;
41
+ /** Reports the latest available conversational transcript. */
42
+ onTranscript(transcript: LiveTranscript | undefined): void;
43
+ /** Reports one terminal stop, optionally carrying its cause. */
44
+ onTerminal(error?: Error): void;
45
+ }
46
+
47
+ /** Dependencies and presentation callbacks for a live session. */
48
+ export interface LiveSessionControllerOptions {
49
+ /** Agent session that performs all delegated coding work. */
50
+ session: AgentSession;
51
+ /** UI callbacks for live session state. */
52
+ callbacks: LiveSessionCallbacks;
53
+ /** Extracts visible assistant text using the caller's normal UI rules. */
54
+ extractAssistantText(message: AssistantMessage): string;
55
+ /** Realtime output voice, defaulting to sol. */
56
+ voice?: string;
57
+ }
58
+
59
+ function errorFrom(cause: unknown): Error {
60
+ return cause instanceof Error ? cause : new Error(String(cause));
61
+ }
62
+
63
+ function clampLevel(level: number): number {
64
+ if (!Number.isFinite(level) || level <= 0) return 0;
65
+ return Math.min(1, level);
66
+ }
67
+
68
+ function microphoneLevel(samples: Float32Array): number {
69
+ if (samples.length === 0) return 0;
70
+ let sumSquares = 0;
71
+ for (let index = 0; index < samples.length; index += 1) {
72
+ const sample = samples[index] ?? 0;
73
+ sumSquares += sample * sample;
74
+ }
75
+ return clampLevel(Math.sqrt(sumSquares / samples.length));
76
+ }
77
+
78
+ function currentUser(): { username: string; firstName: string } {
79
+ let username = "user";
80
+ try {
81
+ const candidate = os.userInfo().username.trim();
82
+ if (candidate) username = candidate;
83
+ } catch {
84
+ // Sandboxed runtimes may not expose OS account information.
85
+ }
86
+ const firstPart = username.split(/[._\-\s]+/).find(part => part.length > 0);
87
+ return { username, firstName: firstPart ?? "there" };
88
+ }
89
+
90
+ /** Coordinates the realtime conversational surface with normal AgentSession turns. */
91
+ export class LiveSessionController {
92
+ readonly #session: AgentSession;
93
+ readonly #callbacks: LiveSessionCallbacks;
94
+ readonly #extractAssistantText: (message: AssistantMessage) => string;
95
+ readonly #voice: string;
96
+
97
+ #transport: CodexLiveTransport | undefined;
98
+ #recorder: AudioCapture | undefined;
99
+ #unsubscribeSession: (() => void) | undefined;
100
+ #sendChain: Promise<void> = Promise.resolve();
101
+ #stopPromise: Promise<void> | undefined;
102
+ #started = false;
103
+ #stopped = false;
104
+ #terminalEmitted = false;
105
+ #failure: Error | undefined;
106
+ #muted = false;
107
+ #phase: LivePhase = "connecting";
108
+ #inputLevel = 0;
109
+ #outputLevel = 0;
110
+ #activeDelegationId: string | undefined;
111
+ #userTranscript = "";
112
+ #assistantTranscript = "";
113
+ #userTranscriptFinal = false;
114
+ #assistantTranscriptFinal = false;
115
+ #userTranscriptTurn = 0;
116
+ #assistantTranscriptTurn = 0;
117
+ #lastTranscript: LiveTranscript | undefined;
118
+
119
+ constructor(options: LiveSessionControllerOptions) {
120
+ this.#session = options.session;
121
+ this.#callbacks = options.callbacks;
122
+ this.#extractAssistantText = options.extractAssistantText;
123
+ this.#voice = options.voice?.trim() || DEFAULT_VOICE;
124
+ }
125
+
126
+ /** Current realtime call phase. */
127
+ get phase(): LivePhase {
128
+ return this.#phase;
129
+ }
130
+
131
+ /** Whether microphone input is currently muted. */
132
+ get muted(): boolean {
133
+ return this.#muted;
134
+ }
135
+
136
+ /** Connects the realtime surface and starts microphone streaming. */
137
+ async start(): Promise<void> {
138
+ if (this.#stopped) {
139
+ throw (
140
+ this.#failure ?? new Error("This live session has already stopped; create a new controller to reconnect.")
141
+ );
142
+ }
143
+ if (this.#started) return;
144
+ this.#started = true;
145
+ this.#emitPhase("connecting", true);
146
+ this.#emitTranscript(undefined);
147
+ if (this.#stopped) {
148
+ throw this.#failure ?? new Error("The live session stopped while starting.");
149
+ }
150
+
151
+ try {
152
+ const user = currentUser();
153
+ const instructions = prompt.render(liveInstructionsTemplate, user);
154
+ const transport = new CodexLiveTransport({
155
+ authStorage: this.#session.modelRegistry.authStorage,
156
+ sessionId: this.#session.sessionId,
157
+ instructions,
158
+ voice: this.#voice,
159
+ callbacks: {
160
+ onEvent: event => this.#guardEvent(() => this.#handleLiveEvent(event)),
161
+ onOutputLevel: level => this.#guardEvent(() => this.#handleOutputLevel(level)),
162
+ },
163
+ });
164
+ this.#transport = transport;
165
+ await transport.connect();
166
+ if (this.#stopped) {
167
+ throw this.#failure ?? new Error("The live session stopped while connecting.");
168
+ }
169
+ this.#unsubscribeSession = this.#session.subscribe(event =>
170
+ this.#guardEvent(() => this.#handleSessionEvent(event)),
171
+ );
172
+ if (this.#muted) await transport.setMuted(true);
173
+ if (this.#stopped) {
174
+ throw this.#failure ?? new Error("The live session stopped before recording began.");
175
+ }
176
+ const recorder = new AudioCapture(16_000, (error, samples) => {
177
+ if (error) {
178
+ this.#reportFailure(error);
179
+ return;
180
+ }
181
+ this.#handleMicrophoneAudio(samples);
182
+ });
183
+ if (this.#stopped) {
184
+ try {
185
+ recorder.stop();
186
+ } catch {
187
+ // Preserve the failure that stopped startup.
188
+ }
189
+ throw this.#failure ?? new Error("The live session stopped while recording began.");
190
+ }
191
+ this.#recorder = recorder;
192
+ this.#refreshAudioPhase();
193
+ } catch (cause) {
194
+ const error = errorFrom(cause);
195
+ this.#reportFailure(error);
196
+ await this.stop();
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ /** Toggles microphone capture while leaving output and the session connected. */
202
+ toggleMute(): void {
203
+ if (this.#stopped) return;
204
+ this.#muted = !this.#muted;
205
+ if (this.#muted) {
206
+ this.#inputLevel = 0;
207
+ this.#emitLevels();
208
+ }
209
+ this.#refreshAudioPhase();
210
+ const transport = this.#transport;
211
+ if (transport) {
212
+ void transport.setMuted(this.#muted).catch(cause => this.#reportFailure(errorFrom(cause)));
213
+ }
214
+ }
215
+
216
+ /** Stops recording, closes the live session, and emits one terminal callback. */
217
+ stop(): Promise<void> {
218
+ if (!this.#stopPromise) this.#stopPromise = this.#stop();
219
+ return this.#stopPromise;
220
+ }
221
+
222
+ async #stop(): Promise<void> {
223
+ this.#stopped = true;
224
+ this.#unsubscribeSession?.();
225
+ this.#unsubscribeSession = undefined;
226
+ let cleanupError: Error | undefined;
227
+
228
+ const recorder = this.#recorder;
229
+ this.#recorder = undefined;
230
+ if (recorder) {
231
+ try {
232
+ recorder.stop();
233
+ } catch (cause) {
234
+ cleanupError = errorFrom(cause);
235
+ }
236
+ }
237
+
238
+ await this.#sendChain;
239
+ const transport = this.#transport;
240
+ this.#transport = undefined;
241
+ if (transport) {
242
+ try {
243
+ await transport.send(buildSessionClose());
244
+ } catch (cause) {
245
+ cleanupError ??= errorFrom(cause);
246
+ }
247
+ try {
248
+ await transport.close();
249
+ } catch (cause) {
250
+ cleanupError ??= errorFrom(cause);
251
+ }
252
+ }
253
+
254
+ if (cleanupError) this.#emitPhaseSafely("error");
255
+ this.#emitTerminal(cleanupError);
256
+ }
257
+
258
+ #guardEvent(handler: () => void): void {
259
+ if (this.#stopped) return;
260
+ try {
261
+ handler();
262
+ } catch (cause) {
263
+ this.#reportFailure(errorFrom(cause));
264
+ }
265
+ }
266
+
267
+ #handleLiveEvent(event: LiveServerEvent): void {
268
+ switch (event.type) {
269
+ case "session.started":
270
+ this.#emitPhase("listening");
271
+ break;
272
+ case "session.updated":
273
+ case "output_audio.delta":
274
+ case "unknown":
275
+ break;
276
+ case "input_transcript.added":
277
+ this.#addTranscript("user", event.item.text);
278
+ break;
279
+ case "output_transcript.added":
280
+ this.#addTranscript("assistant", event.item.text);
281
+ break;
282
+ case "turn.done":
283
+ this.#finishTranscript(event.turn.role, event.turn.transcript);
284
+ break;
285
+ case "delegation.created":
286
+ this.#handleDelegation(event);
287
+ break;
288
+ case "error":
289
+ this.#reportFailure(new Error(event.message));
290
+ break;
291
+ }
292
+ }
293
+
294
+ #handleDelegation(event: Extract<LiveServerEvent, { type: "delegation.created" }>): void {
295
+ let request = "";
296
+ for (const content of event.item.content) {
297
+ if (content.type !== "input_text") continue;
298
+ request += `${request ? "\n" : ""}${content.text}`;
299
+ }
300
+ request = request.trim();
301
+ if (!request) return;
302
+ this.#activeDelegationId = event.item.id;
303
+ this.#emitPhase("working");
304
+ void this.#session
305
+ .sendCustomMessage(
306
+ {
307
+ customType: LIVE_DELEGATION_MESSAGE_TYPE,
308
+ content: request,
309
+ display: true,
310
+ attribution: "agent",
311
+ },
312
+ { triggerTurn: true },
313
+ )
314
+ .catch(cause => this.#reportFailure(errorFrom(cause)));
315
+ }
316
+
317
+ #handleSessionEvent(event: AgentSessionEvent): void {
318
+ if (event.type === "message_end" && event.message.role === "assistant") {
319
+ if (event.message.stopReason === "toolUse") this.#appendProgress(event.message);
320
+ return;
321
+ }
322
+ if (event.type !== "agent_end" || event.isTerminal === false) return;
323
+ this.#appendFinalResponse(event.messages);
324
+ }
325
+
326
+ #appendProgress(message: AssistantMessage): void {
327
+ const delegationId = this.#activeDelegationId;
328
+ if (!delegationId) return;
329
+ const progress = this.#extractAssistantText(message).trim();
330
+ if (!progress) return;
331
+ for (const chunk of chunkLiveContext(progress)) {
332
+ this.#queueSend(buildDelegationContextAppend(delegationId, chunk, "commentary"));
333
+ }
334
+ }
335
+
336
+ #appendFinalResponse(messages: readonly AgentMessage[]): void {
337
+ const delegationId = this.#activeDelegationId;
338
+ if (!delegationId) return;
339
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
340
+ const message = messages[index];
341
+ if (message?.role !== "assistant") continue;
342
+ const text = this.#extractAssistantText(message).trim();
343
+ if (!text) continue;
344
+ const finalContext = prompt.render(agentFinalMessageTemplate, { message: text });
345
+ for (const chunk of chunkLiveContext(finalContext)) {
346
+ this.#queueSend(buildDelegationContextAppend(delegationId, chunk));
347
+ }
348
+ break;
349
+ }
350
+ this.#activeDelegationId = undefined;
351
+ this.#refreshAudioPhase();
352
+ }
353
+
354
+ #handleOutputLevel(level: number): void {
355
+ this.#outputLevel = clampLevel(level);
356
+ this.#emitLevels();
357
+ if (!this.#activeDelegationId) this.#refreshAudioPhase();
358
+ }
359
+
360
+ #handleMicrophoneAudio(samples: Float32Array): void {
361
+ if (this.#stopped || !this.#transport) return;
362
+ if (this.#muted) return;
363
+ this.#inputLevel = microphoneLevel(samples);
364
+ this.#emitLevels();
365
+ const outputActive = this.#outputLevel > OUTPUT_ACTIVE_LEVEL;
366
+ const echoThreshold = Math.max(MIN_BARGE_IN_LEVEL, this.#outputLevel * OUTPUT_ECHO_RATIO);
367
+ if (outputActive && this.#inputLevel < echoThreshold) return;
368
+ try {
369
+ this.#transport.pushAudio(samples);
370
+ } catch (cause) {
371
+ this.#reportFailure(errorFrom(cause));
372
+ }
373
+ }
374
+
375
+ #addTranscript(role: LiveTranscript["role"], text: string): void {
376
+ if (!text) return;
377
+ const current = role === "user" ? this.#userTranscript : this.#assistantTranscript;
378
+ const wasFinal = role === "user" ? this.#userTranscriptFinal : this.#assistantTranscriptFinal;
379
+ let next: string;
380
+ if (!current) {
381
+ this.#startTranscriptTurn(role);
382
+ next = text;
383
+ } else if (wasFinal) {
384
+ if (text === current || current.endsWith(text)) return;
385
+ this.#startTranscriptTurn(role);
386
+ next = text;
387
+ } else if (text.startsWith(current)) {
388
+ next = text;
389
+ } else if (current.endsWith(text)) {
390
+ next = current;
391
+ } else {
392
+ next = current + text;
393
+ }
394
+ this.#storeTranscript(role, next, false);
395
+ }
396
+
397
+ #finishTranscript(role: LiveTranscript["role"], text: string): void {
398
+ if (!text) return;
399
+ const current = role === "user" ? this.#userTranscript : this.#assistantTranscript;
400
+ const wasFinal = role === "user" ? this.#userTranscriptFinal : this.#assistantTranscriptFinal;
401
+ if (!current) {
402
+ this.#startTranscriptTurn(role);
403
+ } else if (wasFinal) {
404
+ if (text === current) return;
405
+ this.#startTranscriptTurn(role);
406
+ }
407
+ const next = !wasFinal && current.startsWith(text) && current.length > text.length ? current : text;
408
+ this.#storeTranscript(role, next, true);
409
+ }
410
+
411
+ #startTranscriptTurn(role: LiveTranscript["role"]): void {
412
+ if (role === "user") {
413
+ this.#userTranscriptTurn += 1;
414
+ } else {
415
+ this.#assistantTranscriptTurn += 1;
416
+ }
417
+ }
418
+
419
+ #storeTranscript(role: LiveTranscript["role"], text: string, final: boolean): void {
420
+ const normalized = text.trim();
421
+ if (!normalized) return;
422
+ const turn = role === "user" ? this.#userTranscriptTurn : this.#assistantTranscriptTurn;
423
+ if (role === "user") {
424
+ this.#userTranscript = normalized;
425
+ this.#userTranscriptFinal = final;
426
+ } else {
427
+ this.#assistantTranscript = normalized;
428
+ this.#assistantTranscriptFinal = final;
429
+ }
430
+ if (
431
+ this.#lastTranscript?.role === role &&
432
+ this.#lastTranscript.turn === turn &&
433
+ this.#lastTranscript.text === normalized &&
434
+ this.#lastTranscript.final === final
435
+ ) {
436
+ return;
437
+ }
438
+ this.#emitTranscript({ role, turn, text: normalized, final });
439
+ }
440
+
441
+ #queueSend(message: LiveClientMessage): void {
442
+ const transport = this.#transport;
443
+ if (!transport || this.#stopped) return;
444
+ this.#sendChain = this.#sendChain
445
+ .then(async () => {
446
+ if (!this.#stopped) await transport.send(message);
447
+ })
448
+ .catch(cause => this.#reportFailure(errorFrom(cause)));
449
+ }
450
+
451
+ #refreshAudioPhase(): void {
452
+ if (this.#stopped) return;
453
+ if (this.#muted) {
454
+ this.#emitPhase("muted");
455
+ } else if (this.#activeDelegationId) {
456
+ this.#emitPhase("working");
457
+ } else if (this.#outputLevel > OUTPUT_ACTIVE_LEVEL) {
458
+ this.#emitPhase("speaking");
459
+ } else {
460
+ this.#emitPhase("listening");
461
+ }
462
+ }
463
+
464
+ #emitPhase(phase: LivePhase, force = false): void {
465
+ if (!force && this.#phase === phase) return;
466
+ this.#phase = phase;
467
+ try {
468
+ this.#callbacks.onPhase(phase);
469
+ } catch (cause) {
470
+ this.#reportFailure(errorFrom(cause));
471
+ }
472
+ }
473
+
474
+ #emitPhaseSafely(phase: LivePhase): void {
475
+ this.#phase = phase;
476
+ try {
477
+ this.#callbacks.onPhase(phase);
478
+ } catch {
479
+ // Terminal callback is the final error boundary for UI failures.
480
+ }
481
+ }
482
+
483
+ #emitLevels(): void {
484
+ try {
485
+ this.#callbacks.onLevels(this.#inputLevel, this.#outputLevel);
486
+ } catch (cause) {
487
+ this.#reportFailure(errorFrom(cause));
488
+ }
489
+ }
490
+
491
+ #emitTranscript(transcript: LiveTranscript | undefined): void {
492
+ this.#lastTranscript = transcript;
493
+ try {
494
+ this.#callbacks.onTranscript(transcript);
495
+ } catch (cause) {
496
+ this.#reportFailure(errorFrom(cause));
497
+ }
498
+ }
499
+
500
+ #reportFailure(error: Error): void {
501
+ if (this.#terminalEmitted) return;
502
+ this.#failure = error;
503
+ this.#emitPhaseSafely("error");
504
+ this.#emitTerminal(error);
505
+ void this.stop();
506
+ }
507
+
508
+ #emitTerminal(error?: Error): void {
509
+ if (this.#terminalEmitted) return;
510
+ this.#terminalEmitted = true;
511
+ try {
512
+ this.#callbacks.onTerminal(error);
513
+ } catch {
514
+ // Nothing remains above the terminal callback to receive its error.
515
+ }
516
+ }
517
+ }
@@ -0,0 +1,3 @@
1
+ "Agent Final Message":
2
+
3
+ {{message}}
@@ -0,0 +1,23 @@
1
+ You are omp Live, the realtime voice surface of one unified coding assistant for {{firstName}} (OS account: {{username}}).
2
+
3
+ <system-conventions>
4
+ RFC 2119 applies to MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, and OPTIONAL. `NEVER` means `MUST NOT`.
5
+ </system-conventions>
6
+
7
+ <critical>
8
+ - You and the omp coding agent are one assistant, not separate agents.
9
+ - You MUST delegate repository work, coding, tool use, and verification to the client backend.
10
+ - You MUST keep conversation natural while the client backend works.
11
+ </critical>
12
+
13
+ The user is speaking to you. You MUST respond directly, briefly, and conversationally. You MUST use speech-friendly phrasing. NEVER use markdown, code blocks, or long lists. NEVER read implementation detail aloud unless requested.
14
+
15
+ The client backend is the same assistant's execution surface. It has the repository context, normal omp AgentSession, coding model, and tools. Coding, investigation, repository changes, commands, or verification? You MUST create a client delegation containing the complete plain-language request and all relevant conversational context. You MUST delegate promptly instead of attempting tool work yourself. A new request during active work MUST create a new delegation so it steers the same backend session.
16
+
17
+ You MUST treat delegation context as your own internal progress and result. NEVER describe the backend as another assistant. You MAY briefly acknowledge active work, but NEVER claim changes, findings, or verification before the backend reports them. Commentary context is silent progress for conversational continuity; NEVER recite it. Context beginning with `"Agent Final Message":` is the backend's final visible answer. You MUST present its useful result naturally as your own without mentioning the label, protocol, delegation, or backend.
18
+
19
+ Greetings, clarification, or ordinary conversation requiring no repository or tools? You MUST answer directly without delegation. You MUST ask a concise clarifying question only when the execution request is genuinely underspecified.
20
+
21
+ <critical>
22
+ You MUST preserve one-assistant continuity: converse here, delegate execution, then communicate the returned result as your own.
23
+ </critical>
@@ -0,0 +1,140 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ buildDelegationContextAppend,
4
+ buildLiveSessionPayload,
5
+ buildSessionClose,
6
+ buildSessionContextAppend,
7
+ CONTEXT_CHUNK_BYTES,
8
+ chunkLiveContext,
9
+ LIVE_MODEL,
10
+ parseLiveServerEvent,
11
+ } from "./protocol";
12
+
13
+ describe("Frameless Bidi server events", () => {
14
+ test("parses a client delegation and keeps only input text content", () => {
15
+ const event = parseLiveServerEvent(
16
+ JSON.stringify({
17
+ type: "delegation.created",
18
+ item: {
19
+ type: "delegation",
20
+ target: "client",
21
+ id: "delegation-7",
22
+ content: [
23
+ { type: "input_text", text: "Inspect the failing build. " },
24
+ { type: "output_text", text: "ignored" },
25
+ { type: "input_text", text: "Repair the root cause." },
26
+ ],
27
+ },
28
+ }),
29
+ );
30
+
31
+ expect(event).toEqual({
32
+ type: "delegation.created",
33
+ item: {
34
+ type: "delegation",
35
+ target: "client",
36
+ id: "delegation-7",
37
+ content: [
38
+ { type: "input_text", text: "Inspect the failing build. " },
39
+ { type: "input_text", text: "Repair the root cause." },
40
+ ],
41
+ },
42
+ });
43
+ });
44
+
45
+ test("parses input and output transcript deltas", () => {
46
+ expect(parseLiveServerEvent({ type: "input_transcript.added", item: { text: "What changed?" } })).toEqual({
47
+ type: "input_transcript.added",
48
+ item: { text: "What changed?" },
49
+ });
50
+ expect(parseLiveServerEvent({ type: "output_transcript.added", item: { text: "I will inspect it." } })).toEqual({
51
+ type: "output_transcript.added",
52
+ item: { text: "I will inspect it." },
53
+ });
54
+ });
55
+
56
+ test("parses completed user and assistant turns", () => {
57
+ expect(
58
+ parseLiveServerEvent({ type: "turn.done", turn: { role: "user", transcript: "Run the checks." } }),
59
+ ).toEqual({ type: "turn.done", turn: { role: "user", transcript: "Run the checks." } });
60
+ expect(
61
+ parseLiveServerEvent({
62
+ type: "turn.done",
63
+ turn: { role: "assistant", transcript: "The checks pass." },
64
+ }),
65
+ ).toEqual({ type: "turn.done", turn: { role: "assistant", transcript: "The checks pass." } });
66
+ });
67
+
68
+ test("parses an output audio delta", () => {
69
+ expect(parseLiveServerEvent('{"type":"output_audio.delta","audio":"AAECAw=="}')).toEqual({
70
+ type: "output_audio.delta",
71
+ audio: "AAECAw==",
72
+ });
73
+ });
74
+
75
+ test("extracts top-level and nested error messages", () => {
76
+ expect(parseLiveServerEvent({ type: "error", message: "call expired" })).toEqual({
77
+ type: "error",
78
+ message: "call expired",
79
+ });
80
+ expect(parseLiveServerEvent({ type: "error", error: { message: "media rejected", code: "bad_media" } })).toEqual({
81
+ type: "error",
82
+ message: "media rejected",
83
+ });
84
+ });
85
+
86
+ test("classifies unsupported events and rejects malformed known events", () => {
87
+ expect(parseLiveServerEvent({ type: "rate_limits.updated", remaining: 3 })).toEqual({
88
+ type: "unknown",
89
+ wireType: "rate_limits.updated",
90
+ });
91
+ expect(parseLiveServerEvent({ type: "output_audio.delta", audio: 12 })).toBeNull();
92
+ expect(parseLiveServerEvent({ type: "turn.done", turn: { role: "tool", transcript: "no" } })).toBeNull();
93
+ expect(parseLiveServerEvent("not json")).toBeNull();
94
+ });
95
+ });
96
+
97
+ describe("Frameless Bidi client payloads", () => {
98
+ test("builds the exact live call session JSON", () => {
99
+ const payload = buildLiveSessionPayload("Be concise.", "marin");
100
+
101
+ expect(LIVE_MODEL).toBe("gpt-live-1-codex");
102
+ expect(JSON.stringify(payload)).toBe(
103
+ '{"model":"gpt-live-1-codex","instructions":"Be concise.","audio":{"output":{"voice":"marin"}},"delegation":{"type":"client"}}',
104
+ );
105
+ });
106
+
107
+ test("builds the exact delegation context JSON", () => {
108
+ const message = buildDelegationContextAppend("delegation-7", "The tests now pass.", "commentary");
109
+
110
+ expect(JSON.stringify(message)).toBe(
111
+ '{"type":"delegation.context.append","delegation_item_id":"delegation-7","channel":"commentary","content":[{"type":"input_text","text":"The tests now pass."}]}',
112
+ );
113
+ });
114
+
115
+ test("omits optional channels and builds session close", () => {
116
+ expect(buildDelegationContextAppend("delegation-8", "Done.")).toEqual({
117
+ type: "delegation.context.append",
118
+ delegation_item_id: "delegation-8",
119
+ content: [{ type: "input_text", text: "Done." }],
120
+ });
121
+ expect(buildSessionContextAppend("Still investigating.", "speakable")).toEqual({
122
+ type: "session.context.append",
123
+ channel: "speakable",
124
+ content: [{ type: "input_text", text: "Still investigating." }],
125
+ });
126
+ expect(buildSessionClose()).toEqual({ type: "session.close" });
127
+ });
128
+ });
129
+
130
+ test("context chunks are UTF-8 safe, bounded, and perfectly reassemble", () => {
131
+ const text = `${"a".repeat(497)}🙂${"é漢🙂".repeat(180)}`;
132
+ const chunks = chunkLiveContext(text);
133
+ const encoder = new TextEncoder();
134
+
135
+ expect(chunks.length).toBeGreaterThan(1);
136
+ expect(chunks.join("")).toBe(text);
137
+ for (const chunk of chunks) {
138
+ expect(encoder.encode(chunk).byteLength).toBeLessThanOrEqual(CONTEXT_CHUNK_BYTES);
139
+ }
140
+ });