@code-yeongyu/senpi 2026.8.22 → 2026.8.24

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 (230) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/bun-runtime.d.ts +58 -0
  3. package/dist/bun-runtime.d.ts.map +1 -0
  4. package/dist/bun-runtime.js +163 -0
  5. package/dist/bun-runtime.js.map +1 -0
  6. package/dist/cli.js +45 -2
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/agent-abort-provenance.d.ts +5 -5
  9. package/dist/core/agent-abort-provenance.d.ts.map +1 -1
  10. package/dist/core/agent-abort-provenance.js +3 -0
  11. package/dist/core/agent-abort-provenance.js.map +1 -1
  12. package/dist/core/agent-session.d.ts +10 -2
  13. package/dist/core/agent-session.d.ts.map +1 -1
  14. package/dist/core/agent-session.js +85 -26
  15. package/dist/core/agent-session.js.map +1 -1
  16. package/dist/core/agent-settled-delivery.d.ts +13 -1
  17. package/dist/core/agent-settled-delivery.d.ts.map +1 -1
  18. package/dist/core/agent-settled-delivery.js +33 -2
  19. package/dist/core/agent-settled-delivery.js.map +1 -1
  20. package/dist/core/extensions/builtin/goal/agent-end-continuation.d.ts.map +1 -1
  21. package/dist/core/extensions/builtin/goal/agent-end-continuation.js +10 -0
  22. package/dist/core/extensions/builtin/goal/agent-end-continuation.js.map +1 -1
  23. package/dist/core/extensions/builtin/goal/cache-warm-renderer.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +2 -3
  25. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js.map +1 -1
  26. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +6 -0
  27. package/dist/core/extensions/builtin/goal/cache-warm.d.ts.map +1 -1
  28. package/dist/core/extensions/builtin/goal/cache-warm.js +43 -0
  29. package/dist/core/extensions/builtin/goal/cache-warm.js.map +1 -1
  30. package/dist/core/extensions/builtin/goal/continuation.d.ts +1 -1
  31. package/dist/core/extensions/builtin/goal/continuation.d.ts.map +1 -1
  32. package/dist/core/extensions/builtin/goal/continuation.js +1 -1
  33. package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
  34. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  35. package/dist/core/extensions/builtin/goal/index.js +0 -8
  36. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  37. package/dist/core/extensions/builtin/goal/monitor-continuation-types.d.ts +1 -0
  38. package/dist/core/extensions/builtin/goal/monitor-continuation-types.d.ts.map +1 -1
  39. package/dist/core/extensions/builtin/goal/monitor-continuation-types.js.map +1 -1
  40. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts +2 -1
  41. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  42. package/dist/core/extensions/builtin/goal/monitor-continuation.js +31 -3
  43. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  44. package/dist/core/extensions/builtin/goal/wait-ticker.d.ts.map +1 -1
  45. package/dist/core/extensions/builtin/goal/wait-ticker.js +14 -9
  46. package/dist/core/extensions/builtin/goal/wait-ticker.js.map +1 -1
  47. package/dist/core/extensions/builtin/webfetch/webfetch/fetcher.d.ts.map +1 -1
  48. package/dist/core/extensions/builtin/webfetch/webfetch/fetcher.js +5 -23
  49. package/dist/core/extensions/builtin/webfetch/webfetch/fetcher.js.map +1 -1
  50. package/dist/core/extensions/builtin/webfetch/webfetch/response-body.d.ts +11 -0
  51. package/dist/core/extensions/builtin/webfetch/webfetch/response-body.d.ts.map +1 -0
  52. package/dist/core/extensions/builtin/webfetch/webfetch/response-body.js +54 -0
  53. package/dist/core/extensions/builtin/webfetch/webfetch/response-body.js.map +1 -0
  54. package/dist/core/model-config-schema.d.ts +12 -12
  55. package/dist/core/model-config-schema.js +2 -2
  56. package/dist/core/model-config-schema.js.map +1 -1
  57. package/dist/core/model-resolver.js +2 -2
  58. package/dist/core/model-resolver.js.map +1 -1
  59. package/dist/core/session-work-barrier.d.ts.map +1 -1
  60. package/dist/core/session-work-barrier.js +38 -1
  61. package/dist/core/session-work-barrier.js.map +1 -1
  62. package/dist/core/tools/read.d.ts.map +1 -1
  63. package/dist/core/tools/read.js +5 -0
  64. package/dist/core/tools/read.js.map +1 -1
  65. package/dist/modes/interactive/aborted-error-label.d.ts +2 -1
  66. package/dist/modes/interactive/aborted-error-label.d.ts.map +1 -1
  67. package/dist/modes/interactive/aborted-error-label.js +20 -5
  68. package/dist/modes/interactive/aborted-error-label.js.map +1 -1
  69. package/dist/modes/interactive/components/status-indicator.d.ts +3 -0
  70. package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/status-indicator.js +7 -1
  72. package/dist/modes/interactive/components/status-indicator.js.map +1 -1
  73. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  74. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.js +50 -10
  76. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  77. package/dist/modes/interactive/streaming-reveal.d.ts +8 -0
  78. package/dist/modes/interactive/streaming-reveal.d.ts.map +1 -1
  79. package/dist/modes/interactive/streaming-reveal.js +12 -0
  80. package/dist/modes/interactive/streaming-reveal.js.map +1 -1
  81. package/dist/senpi +45 -2
  82. package/docs/AGENTS.md +3 -1
  83. package/examples/AGENTS.md +21 -6
  84. package/examples/extensions/AGENTS.md +50 -0
  85. package/node_modules/@anthropic-ai/claude-agent-sdk/bridge.mjs +90 -88
  86. package/node_modules/@anthropic-ai/claude-agent-sdk/browser-sdk.js +40 -40
  87. package/node_modules/@anthropic-ai/claude-agent-sdk/manifest.json +20 -21
  88. package/node_modules/@anthropic-ai/claude-agent-sdk/manifest.zst.json +24 -25
  89. package/node_modules/@anthropic-ai/claude-agent-sdk/package.json +2 -2
  90. package/node_modules/@anthropic-ai/claude-agent-sdk/sdk-tools.d.ts +102 -5
  91. package/node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts +31 -15
  92. package/node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs +80 -78
  93. package/node_modules/@aws-sdk/client-bedrock-runtime/dist-cjs/index.js +1 -1
  94. package/node_modules/@aws-sdk/client-bedrock-runtime/package.json +13 -13
  95. package/node_modules/@aws-sdk/core/package.json +5 -5
  96. package/node_modules/@aws-sdk/credential-provider-env/package.json +5 -5
  97. package/node_modules/@aws-sdk/credential-provider-http/package.json +7 -7
  98. package/node_modules/@aws-sdk/credential-provider-ini/package.json +12 -12
  99. package/node_modules/@aws-sdk/credential-provider-login/package.json +6 -6
  100. package/node_modules/@aws-sdk/credential-provider-node/package.json +10 -10
  101. package/node_modules/@aws-sdk/credential-provider-process/package.json +5 -5
  102. package/node_modules/@aws-sdk/credential-provider-sso/package.json +7 -7
  103. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +6 -6
  104. package/node_modules/@aws-sdk/eventstream-handler-node/package.json +4 -4
  105. package/node_modules/@aws-sdk/middleware-eventstream/package.json +4 -4
  106. package/node_modules/@aws-sdk/middleware-websocket/package.json +6 -6
  107. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
  108. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
  109. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
  110. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
  111. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
  112. package/node_modules/@aws-sdk/nested-clients/package.json +8 -8
  113. package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +3 -3
  114. package/node_modules/@aws-sdk/token-providers/package.json +6 -6
  115. package/node_modules/@aws-sdk/types/package.json +2 -2
  116. package/node_modules/@aws-sdk/xml-builder/package.json +2 -2
  117. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +46 -0
  118. package/node_modules/@code-yeongyu/senpi-codemode/README.md +5 -0
  119. package/node_modules/@code-yeongyu/senpi-codemode/package.json +5 -5
  120. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/eval-notifier.ts +16 -4
  121. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/runtime-factory.ts +4 -1
  122. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/runtime-info.ts +41 -0
  123. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +9 -3
  124. package/node_modules/@code-yeongyu/senpi-codemode/src/interpreters/detect.ts +17 -4
  125. package/node_modules/@code-yeongyu/senpi-codemode/src/interpreters/resolve-command.ts +68 -0
  126. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/AGENTS.md +40 -0
  127. package/node_modules/@code-yeongyu/senpi-codemode/src/timeouts/idle-timeout.ts +33 -4
  128. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/AGENTS.md +40 -0
  129. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-runtime.ts +5 -1
  130. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-manager.ts +1 -1
  131. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-notification.ts +3 -8
  132. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-notification-queue.ts +2 -6
  133. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool-options.ts +3 -0
  134. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +2 -0
  135. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +60 -3
  136. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/runtime-label.ts +49 -0
  137. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +13 -0
  138. package/node_modules/@earendil-works/pi-agent-core/package.json +4 -4
  139. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +5 -1
  140. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  141. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +13 -5
  142. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  143. package/node_modules/@earendil-works/pi-ai/dist/cursor/composer-prompt.d.ts +27 -0
  144. package/node_modules/@earendil-works/pi-ai/dist/cursor/composer-prompt.d.ts.map +1 -0
  145. package/node_modules/@earendil-works/pi-ai/dist/cursor/composer-prompt.js +54 -0
  146. package/node_modules/@earendil-works/pi-ai/dist/cursor/composer-prompt.js.map +1 -0
  147. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  148. package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
  149. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  150. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
  151. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  152. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  153. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/markers.d.ts +4 -0
  154. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/markers.d.ts.map +1 -1
  155. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/markers.js +14 -0
  156. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/markers.js.map +1 -1
  157. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/recovery-stream.d.ts.map +1 -1
  158. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/recovery-stream.js +65 -35
  159. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/recovery-stream.js.map +1 -1
  160. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/thinking-recovery.d.ts.map +1 -1
  161. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/thinking-recovery.js +40 -12
  162. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/kimi-xtml/thinking-recovery.js.map +1 -1
  163. package/node_modules/@earendil-works/pi-ai/package.json +4 -4
  164. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  165. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  166. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  167. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +7 -4
  168. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  169. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  170. package/node_modules/@smithy/signature-v4/dist-cjs/index.js +19 -7
  171. package/node_modules/@smithy/signature-v4/dist-es/HeaderFormatter.js +4 -1
  172. package/node_modules/@smithy/signature-v4/dist-es/getCanonicalQuery.js +4 -1
  173. package/node_modules/@smithy/signature-v4/dist-es/getPayloadHash.js +4 -1
  174. package/node_modules/@smithy/signature-v4/dist-es/headerUtil.js +10 -3
  175. package/node_modules/@smithy/signature-v4/dist-es/moveHeadersToQuery.js +4 -1
  176. package/node_modules/@smithy/signature-v4/dist-es/prepareRequest.js +4 -1
  177. package/node_modules/@smithy/signature-v4/package.json +2 -2
  178. package/node_modules/typebox/build/format/email.mjs +1 -1
  179. package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
  180. package/node_modules/typebox/build/format/json_pointer.d.mts +2 -2
  181. package/node_modules/typebox/build/format/json_pointer.mjs +2 -2
  182. package/node_modules/typebox/build/format/json_pointer_uri_fragment.d.mts +2 -2
  183. package/node_modules/typebox/build/format/json_pointer_uri_fragment.mjs +2 -2
  184. package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
  185. package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
  186. package/node_modules/typebox/build/system/memory/assign.mjs +2 -1
  187. package/node_modules/typebox/build/system/memory/create.mjs +3 -4
  188. package/node_modules/typebox/build/system/memory/discard.mjs +2 -2
  189. package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
  190. package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
  191. package/node_modules/typebox/build/system/memory/update.mjs +3 -2
  192. package/node_modules/typebox/package.json +1 -1
  193. package/package.json +36 -36
  194. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/LICENSE +0 -201
  195. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/README.md +0 -62
  196. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-cjs/index.js +0 -154
  197. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/constants.js +0 -2
  198. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromEnvSigningName.js +0 -16
  199. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromSso.js +0 -80
  200. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromStatic.js +0 -8
  201. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getNewSsoOidcToken.js +0 -11
  202. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getSsoOidcClient.js +0 -10
  203. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/index.js +0 -4
  204. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/nodeProvider.js +0 -5
  205. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenExpiry.js +0 -7
  206. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenKey.js +0 -7
  207. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/writeSSOTokenToFile.js +0 -8
  208. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/constants.d.ts +0 -8
  209. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/fromEnvSigningName.d.ts +0 -18
  210. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/fromSso.d.ts +0 -12
  211. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/fromStatic.d.ts +0 -9
  212. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/getNewSsoOidcToken.d.ts +0 -8
  213. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/getSsoOidcClient.d.ts +0 -7
  214. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/index.d.ts +0 -7
  215. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/nodeProvider.d.ts +0 -18
  216. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/constants.d.ts +0 -3
  217. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/fromEnvSigningName.d.ts +0 -8
  218. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/fromSso.d.ts +0 -10
  219. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/fromStatic.d.ts +0 -5
  220. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/getNewSsoOidcToken.d.ts +0 -9
  221. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/getSsoOidcClient.d.ts +0 -7
  222. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/index.d.ts +0 -7
  223. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/nodeProvider.d.ts +0 -3
  224. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/validateTokenExpiry.d.ts +0 -2
  225. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/validateTokenKey.d.ts +0 -1
  226. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/ts3.4/writeSSOTokenToFile.d.ts +0 -2
  227. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/validateTokenExpiry.d.ts +0 -5
  228. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/validateTokenKey.d.ts +0 -4
  229. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-types/writeSSOTokenToFile.d.ts +0 -5
  230. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/package.json +0 -69
@@ -3,10 +3,17 @@ import type { EvalDetachedCellNotification, EvalDetachedCellNotifier } from "../
3
3
 
4
4
  const NON_INTERACTIVE_MODES = new Set(["print", "json"]);
5
5
 
6
+ /** Provenance marker for agent-internal detached-cell notices. */
7
+ export const EVAL_NOTIFICATION_CUSTOM_TYPE = "senpi-codemode:notification";
8
+
6
9
  export type EvalNotifyMode = "wake" | "next-turn" | "off";
7
10
 
8
11
  export interface EvalNotifierDeps {
9
- readonly sendUserMessage: (content: string, options?: { deliverAs?: "steer" | "followUp" }) => void;
12
+ /** Deliver a model-visible notification without rendering synthetic user input. */
13
+ readonly sendMessage: (
14
+ message: { customType: string; content: string; display: boolean },
15
+ options: { triggerTurn: boolean; deliverAs: "steer" | "followUp" },
16
+ ) => void;
10
17
  readonly getContext: () => ExtensionContext | undefined;
11
18
  readonly getMode: () => EvalNotifyMode;
12
19
  }
@@ -33,8 +40,13 @@ export class EvalNotifier implements EvalDetachedCellNotifier {
33
40
  const pending = cells.filter((cell) => !this.#notified.has(cell.cellId));
34
41
  if (pending.length === 0) return;
35
42
  for (const cell of pending) this.#notified.add(cell.cellId);
36
- this.#deps.sendUserMessage(pending.map((cell) => cell.content).join("\n\n"), {
37
- deliverAs: mode === "wake" ? "steer" : "followUp",
38
- });
43
+ this.#deps.sendMessage(
44
+ {
45
+ customType: EVAL_NOTIFICATION_CUSTOM_TYPE,
46
+ content: pending.map((cell) => cell.content).join("\n\n"),
47
+ display: false,
48
+ },
49
+ { triggerTurn: true, deliverAs: mode === "wake" ? "steer" : "followUp" },
50
+ );
39
51
  }
40
52
  }
@@ -14,7 +14,8 @@ import {
14
14
  type InterpreterAvailability,
15
15
  } from "../interpreters/detect.ts";
16
16
  import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
17
- import type { EnabledEvalLanguages, EvalLanguage } from "../tool/types.ts";
17
+ import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
18
+ import { jsRuntimeInfo, runtimesFromAvailability } from "./runtime-info.ts";
18
19
  import {
19
20
  type CodemodeSessionManager,
20
21
  type CreateCodemodeSessionManagerOptions,
@@ -39,6 +40,7 @@ export type SessionRuntime = {
39
40
  readonly parallelPoolWidth: number;
40
41
  readonly manager: CodemodeSessionManager;
41
42
  readonly enabledLanguages: EnabledEvalLanguages;
43
+ readonly runtimes: EvalRuntimes;
42
44
  readonly settings: ResolvedCodemodeSettings;
43
45
  readonly artifactsDir: string;
44
46
  readonly executeTool: AgentExecuteTool;
@@ -82,6 +84,7 @@ export async function createRuntime(
82
84
  parallelPoolWidth,
83
85
  manager,
84
86
  enabledLanguages,
87
+ runtimes: runtimesFromAvailability(availability, jsRuntimeInfo()),
85
88
  settings,
86
89
  artifactsDir: artifacts.dir,
87
90
  executeTool,
@@ -0,0 +1,41 @@
1
+ import type { InterpreterAvailability } from "../interpreters/detect.ts";
2
+ import type { EvalLanguage, EvalRuntimeInfo, EvalRuntimes } from "../tool/types.ts";
3
+
4
+ export interface JsRuntimeVersions {
5
+ readonly node: string;
6
+ readonly bun?: string | undefined;
7
+ }
8
+
9
+ /** Identity of the in-process JS kernel host: bun when its marker exists, node otherwise. */
10
+ export function jsRuntimeInfo(
11
+ versions: JsRuntimeVersions = process.versions,
12
+ execPath: string = process.execPath,
13
+ ): EvalRuntimeInfo {
14
+ const bun = versions.bun;
15
+ if (bun !== undefined && bun.length > 0) return { name: "bun", version: bun, path: execPath };
16
+ return { name: "node", version: versions.node, path: execPath };
17
+ }
18
+
19
+ /** Short host-line segment, e.g. "node 26.7.0" or "bun 1.4.0". */
20
+ export function jsRuntimeLabel(versions: JsRuntimeVersions = process.versions): string {
21
+ const info = jsRuntimeInfo(versions, "");
22
+ return `${info.name} ${info.version}`;
23
+ }
24
+
25
+ const subprocessRuntimeNames = { py: "python", rb: "ruby", jl: "julia" } as const;
26
+ const subprocessLanguages = ["py", "rb", "jl"] as const;
27
+
28
+ /** Maps detected interpreters to display runtimes, preferring resolved absolute paths. */
29
+ export function runtimesFromAvailability(availability: InterpreterAvailability, js: EvalRuntimeInfo): EvalRuntimes {
30
+ const runtimes: Partial<Record<EvalLanguage, EvalRuntimeInfo>> = { js };
31
+ for (const language of subprocessLanguages) {
32
+ const detected = availability[language].detected;
33
+ if (!detected.ok) continue;
34
+ runtimes[language] = {
35
+ name: subprocessRuntimeNames[language],
36
+ version: detected.version,
37
+ path: detected.resolvedPath ?? detected.path,
38
+ };
39
+ }
40
+ return runtimes;
41
+ }
@@ -13,6 +13,7 @@ import {
13
13
  enabledLanguagesFrom,
14
14
  type SessionRuntime,
15
15
  } from "./extension/runtime-factory.ts";
16
+ import { jsRuntimeInfo, jsRuntimeLabel } from "./extension/runtime-info.ts";
16
17
  import type { CodemodeSessionManager, CreateCodemodeSessionManagerOptions } from "./extension/session-manager.ts";
17
18
  import { SessionManagerProxy } from "./extension/session-manager-proxy.ts";
18
19
  import { WAKE_SOURCE_STATE_EVENT, type WakeSourceState } from "./extension/wake-source-state.ts";
@@ -43,7 +44,10 @@ export interface CodemodeExtensionAPI {
43
44
  executeTool: AgentExecuteTool;
44
45
  getActiveTools(): string[];
45
46
  getAllTools(): readonly EvalSchemaToolInfo[];
46
- sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp" }): void;
47
+ sendMessage(
48
+ message: { customType: string; content: string; display: boolean },
49
+ options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
50
+ ): void;
47
51
  /** Optional host event bus; a host without one turns extension event emission into a harmless no-op. */
48
52
  events?: { emit(name: string, data: unknown): void };
49
53
  /** Optional host RPC surface for forwarding extension-owned events to connected clients. */
@@ -68,7 +72,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
68
72
  let activeContext: ExtensionContext | undefined;
69
73
  let activeCells: EvalDetachedCellManager | undefined;
70
74
  const notifier = new EvalNotifier({
71
- sendUserMessage: (content, notifyOptions) => pi.sendUserMessage(content, notifyOptions),
75
+ sendMessage: (message, notifyOptions) => pi.sendMessage(message, notifyOptions),
72
76
  getContext: () => activeContext,
73
77
  getMode: () => "wake",
74
78
  });
@@ -119,6 +123,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
119
123
  spawns: runtime.spawns,
120
124
  spawnDefaultAgent: runtime.settings.taskTools.task,
121
125
  hostLine: hostLine(),
126
+ runtimes: runtime.runtimes,
122
127
  ...(modelId === undefined ? {} : { modelId }),
123
128
  }),
124
129
  );
@@ -152,6 +157,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
152
157
  executionTracker: manager,
153
158
  renderers,
154
159
  hostLine: hostLine(),
160
+ runtimes: { js: jsRuntimeInfo() },
155
161
  }),
156
162
  );
157
163
  pi.registerRemovedToolHint(
@@ -206,7 +212,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
206
212
 
207
213
  function hostLine(): string {
208
214
  const cpu = os.cpus()[0]?.model?.trim();
209
- return [`${os.platform()} ${os.arch()}`, cpu, `${os.availableParallelism()} cores`]
215
+ return [`${os.platform()} ${os.arch()}`, cpu, `${os.availableParallelism()} cores`, jsRuntimeLabel()]
210
216
  .filter((part): part is string => !!part)
211
217
  .join(" \u00b7 ");
212
218
  }
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { platform as currentPlatform } from "node:os";
3
3
  import { promisify } from "node:util";
4
4
  import type { CodemodeSettings } from "../config/settings.ts";
5
+ import { resolveCommandPath as defaultResolveCommandPath, type ResolveCommandPath } from "./resolve-command.ts";
5
6
 
6
7
  const execFileAsync = promisify(execFile);
7
8
  const probeTimeoutMs = 3_000;
@@ -10,8 +11,11 @@ export type CodemodeLanguage = "py" | "js" | "rb" | "jl";
10
11
 
11
12
  export interface InterpreterDetected {
12
13
  readonly ok: true;
14
+ /** The probe command line that answered, e.g. "python3" or "py -3". */
13
15
  readonly path: string;
14
16
  readonly version: string;
17
+ /** Absolute executable path resolved from PATH, when resolution succeeded. */
18
+ readonly resolvedPath?: string;
15
19
  }
16
20
 
17
21
  export interface InterpreterUnavailable {
@@ -34,6 +38,7 @@ export interface CreateInterpreterDetectorOptions {
34
38
  readonly platform?: NodeJS.Platform;
35
39
  readonly execFile?: ExecFileProbe;
36
40
  readonly nodeVersion?: string;
41
+ readonly resolveCommandPath?: ResolveCommandPath;
37
42
  }
38
43
 
39
44
  export interface InterpreterDetector {
@@ -55,6 +60,7 @@ export function createInterpreterDetector(options: CreateInterpreterDetectorOpti
55
60
  const probe = options.execFile ?? defaultExecFileProbe;
56
61
  const hostPlatform = options.platform ?? currentPlatform();
57
62
  const nodeVersion = options.nodeVersion ?? process.versions.node;
63
+ const resolveCommand = options.resolveCommandPath ?? defaultResolveCommandPath;
58
64
  const cache = new Map<CodemodeLanguage, Promise<InterpreterDetection>>();
59
65
 
60
66
  return {
@@ -64,7 +70,7 @@ export function createInterpreterDetector(options: CreateInterpreterDetectorOpti
64
70
  return cached;
65
71
  }
66
72
 
67
- const pending = detectUncached(language, hostPlatform, probe, nodeVersion);
73
+ const pending = detectUncached(language, hostPlatform, probe, nodeVersion, resolveCommand);
68
74
  cache.set(language, pending);
69
75
  return pending;
70
76
  },
@@ -99,13 +105,14 @@ async function detectUncached(
99
105
  hostPlatform: NodeJS.Platform,
100
106
  probe: ExecFileProbe,
101
107
  nodeVersion: string,
108
+ resolveCommand: ResolveCommandPath,
102
109
  ): Promise<InterpreterDetection> {
103
110
  if (language === "js") {
104
111
  return { ok: true, path: "node", version: nodeVersion };
105
112
  }
106
113
 
107
114
  for (const candidate of candidatesFor(language, hostPlatform)) {
108
- const result = await probeCandidate(candidate, probe);
115
+ const result = await probeCandidate(candidate, probe, resolveCommand);
109
116
  if (result.ok) {
110
117
  return result;
111
118
  }
@@ -114,12 +121,18 @@ async function detectUncached(
114
121
  return unavailable;
115
122
  }
116
123
 
117
- async function probeCandidate(candidate: string, probe: ExecFileProbe): Promise<InterpreterDetection> {
124
+ async function probeCandidate(
125
+ candidate: string,
126
+ probe: ExecFileProbe,
127
+ resolveCommand: ResolveCommandPath,
128
+ ): Promise<InterpreterDetection> {
118
129
  const invocation = candidateInvocation(candidate);
119
130
  try {
120
131
  const result = await probe(invocation.command, [...invocation.args, "--version"], { timeoutMs: probeTimeoutMs });
121
132
  const version = parseVersion(`${result.stdout}\n${result.stderr}`);
122
- return version === null ? unavailable : { ok: true, path: candidate, version };
133
+ if (version === null) return unavailable;
134
+ const resolvedPath = resolveCommand(invocation.command);
135
+ return { ok: true, path: candidate, version, ...(resolvedPath === undefined ? {} : { resolvedPath }) };
123
136
  } catch {
124
137
  return unavailable;
125
138
  }
@@ -0,0 +1,68 @@
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { delimiter, isAbsolute, resolve as resolvePath } from "node:path";
3
+
4
+ export interface ResolveCommandPathOptions {
5
+ readonly env?: NodeJS.ProcessEnv;
6
+ readonly platform?: NodeJS.Platform;
7
+ readonly cwd?: string;
8
+ }
9
+
10
+ export type ResolveCommandPath = (command: string, options?: ResolveCommandPathOptions) => string | undefined;
11
+
12
+ const defaultWindowsPathExt = ".COM;.EXE;.BAT;.CMD";
13
+
14
+ /**
15
+ * Resolves a bare command name to the absolute executable path a spawn would
16
+ * use, by scanning PATH without spawning a process. Returns undefined when the
17
+ * command cannot be resolved; callers treat that as "no display path known".
18
+ */
19
+ export function resolveCommandPath(command: string, options: ResolveCommandPathOptions = {}): string | undefined {
20
+ if (command.length === 0) return undefined;
21
+ const env = options.env ?? process.env;
22
+ const platform = options.platform ?? process.platform;
23
+ const isWindows = platform === "win32";
24
+ if (command.includes("/") || (isWindows && command.includes("\\"))) {
25
+ const absolute = isAbsolute(command) ? command : resolvePath(options.cwd ?? process.cwd(), command);
26
+ return firstExecutable(candidatesFor(absolute, isWindows, env), isWindows);
27
+ }
28
+ const pathValue = env.PATH ?? env.Path ?? "";
29
+ if (pathValue.length === 0) return undefined;
30
+ for (const directory of pathValue.split(delimiter)) {
31
+ if (directory.length === 0) continue;
32
+ const base = `${directory}${directory.endsWith("/") || directory.endsWith("\\") ? "" : pathSeparatorFor(directory, isWindows)}${command}`;
33
+ const found = firstExecutable(candidatesFor(base, isWindows, env), isWindows);
34
+ if (found !== undefined) return found;
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ function pathSeparatorFor(directory: string, isWindows: boolean): string {
40
+ if (isWindows && directory.includes("\\") && !directory.includes("/")) return "\\";
41
+ return "/";
42
+ }
43
+
44
+ function candidatesFor(base: string, isWindows: boolean, env: NodeJS.ProcessEnv): readonly string[] {
45
+ if (!isWindows) return [base];
46
+ const extensions = (env.PATHEXT ?? defaultWindowsPathExt)
47
+ .split(";")
48
+ .map((extension) => extension.trim())
49
+ .filter((extension) => extension.startsWith("."));
50
+ const candidates = [base];
51
+ for (const extension of extensions) {
52
+ candidates.push(`${base}${extension.toLowerCase()}`, `${base}${extension}`);
53
+ }
54
+ return candidates;
55
+ }
56
+
57
+ function firstExecutable(candidates: readonly string[], isWindows: boolean): string | undefined {
58
+ for (const candidate of candidates) {
59
+ try {
60
+ if (!statSync(candidate).isFile()) continue;
61
+ if (!isWindows) accessSync(candidate, constants.X_OK);
62
+ return candidate;
63
+ } catch {
64
+ // Missing or non-executable candidate: keep scanning the remaining ones.
65
+ }
66
+ }
67
+ return undefined;
68
+ }
@@ -0,0 +1,40 @@
1
+ # src/kernels
2
+
3
+ Persistent kernels for four runtimes plus shared subprocess lifecycle. Earned
4
+ by score 13 — distinct multi-runtime domain (31 files, TS hosts plus embedded
5
+ runner/prelude assets).
6
+
7
+ ## WHERE TO LOOK
8
+
9
+ | Task | Path |
10
+ | --- | --- |
11
+ | JavaScript host API | `js/context-manager.ts` (`JavaScriptKernel`), `js/worker-host.ts` |
12
+ | JS worker runtime, entries | `js/worker-runtime.js`, `js/worker-entry.js`, `js/inline-worker-entry.js`, `js/inline-worker.ts`, `js/worker-core.js` (+ `worker-core.d.ts`) |
13
+ | JS import rewriting, queueing | `js/rewrite-imports.ts`, `js/run-queue.ts`, `js/prelude.ts`, `js/local-module-loader.ts` |
14
+ | Python kernel | `py/kernel.ts`, `py/transport.ts`, `py/process.ts`, `py/prelude.py` |
15
+ | Ruby kernel | `rb/kernel.ts` + `rb/prelude.rb`, `rb/runner.rb` |
16
+ | Julia kernel | `jl/kernel.ts` + `jl/prelude.jl`, `jl/runner.jl` |
17
+ | Shared subprocess layer | `shared/subprocess-kernel.ts`, `subprocess-{contract,process,queue,run}.ts`, `runtime-asset.ts` |
18
+
19
+ ## CONVENTIONS
20
+
21
+ - Each language dir pairs a typed TS host/controller with an embedded runner or
22
+ prelude asset; `shared/runtime-asset.ts` ships them.
23
+ - Transport messages are discriminated by string `type` (`ready`, `result`,
24
+ `tool-call`, `closed`, `init-failed`, ...) exchanged as framed bridge
25
+ messages, one JSON line per frame.
26
+ - JS persistent cell bindings are rewritten onto `globalThis`; imports are
27
+ AST-parsed (Babel) and rewritten to bridge-compatible dynamic imports.
28
+ - JS runs on worker threads with an inline-worker fallback; py/rb/jl run as
29
+ framed subprocesses through `shared/`.
30
+ - Subprocess retirement/restart, worker recovery, timeout, and interrupt
31
+ semantics live here, never in the tool layer.
32
+
33
+ ## ANTI-PATTERNS
34
+
35
+ - Never treat arbitrary objects as bridge messages without discriminant
36
+ validation.
37
+ - Never rewrite imports by filename/regex — `rewrite-imports.ts` applies
38
+ source-position edits from the parsed program.
39
+ - An optional interpreter being absent (py/rb/jl not installed) is a capability
40
+ gap, not an installation failure or error path.
@@ -3,9 +3,18 @@ export interface IdleTimeoutEvent {
3
3
  readonly error: Error;
4
4
  }
5
5
 
6
+ /**
7
+ * Upper bound on how long a single host bridge call may suspend a cell's idle watchdog.
8
+ * Generous enough for a long build or a slow model call, short enough that a bridge call which
9
+ * never returns cannot park the cell — and with it the agent loop — until the 1800s hard limit.
10
+ */
11
+ export const DEFAULT_MAX_PAUSE_GRACE_MS = 600_000;
12
+
6
13
  export interface IdleTimeoutOptions {
7
14
  readonly cellId: string;
8
15
  readonly timeoutMs: number;
16
+ /** Defaults to {@link DEFAULT_MAX_PAUSE_GRACE_MS}; floored at `timeoutMs` so a pause never shortens the budget. */
17
+ readonly maxPauseGraceMs?: number;
9
18
  readonly onTimeout: (event: IdleTimeoutEvent) => void;
10
19
  }
11
20
 
@@ -20,7 +29,9 @@ export class IdleTimeout implements TimeoutPauseHandle {
20
29
  readonly #controller = new AbortController();
21
30
  readonly signal = this.#controller.signal;
22
31
  readonly timeoutMs: number;
32
+ readonly maxPauseGraceMs: number;
23
33
  #deadlineMs: number;
34
+ #pausedDeadlineMs: number | undefined;
24
35
  #timer: ReturnType<typeof setTimeout> | undefined;
25
36
  #pauseDepth = 0;
26
37
  #settled = false;
@@ -28,22 +39,32 @@ export class IdleTimeout implements TimeoutPauseHandle {
28
39
  constructor(options: IdleTimeoutOptions) {
29
40
  this.#cellId = options.cellId;
30
41
  this.timeoutMs = Math.max(1, Math.floor(options.timeoutMs));
42
+ this.maxPauseGraceMs = Math.max(
43
+ this.timeoutMs,
44
+ Math.floor(options.maxPauseGraceMs ?? DEFAULT_MAX_PAUSE_GRACE_MS),
45
+ );
31
46
  this.#deadlineMs = Date.now() + this.timeoutMs;
32
47
  this.#onTimeout = options.onTimeout;
33
48
  this.#arm(this.timeoutMs);
34
49
  }
35
50
 
51
+ /**
52
+ * Suspends the idle deadline for the duration of a host bridge call, but only up to the pause grace:
53
+ * the cell still expires if the call never returns. Nested pauses share the outermost pause's deadline.
54
+ */
36
55
  pause(): void {
37
56
  if (this.#settled) return;
38
57
  this.#pauseDepth++;
39
58
  if (this.#pauseDepth !== 1) return;
40
- this.#clearTimer();
59
+ this.#pausedDeadlineMs = Date.now() + this.maxPauseGraceMs;
60
+ this.#arm(this.maxPauseGraceMs);
41
61
  }
42
62
 
43
63
  resume(): void {
44
64
  if (this.#settled || this.#pauseDepth === 0) return;
45
65
  this.#pauseDepth--;
46
66
  if (this.#pauseDepth > 0) return;
67
+ this.#pausedDeadlineMs = undefined;
47
68
  this.#deadlineMs = Date.now() + this.timeoutMs;
48
69
  this.#arm(this.timeoutMs);
49
70
  }
@@ -51,6 +72,7 @@ export class IdleTimeout implements TimeoutPauseHandle {
51
72
  dispose(): void {
52
73
  if (this.#settled) return;
53
74
  this.#settled = true;
75
+ this.#pausedDeadlineMs = undefined;
54
76
  this.#clearTimer();
55
77
  }
56
78
 
@@ -68,15 +90,22 @@ export class IdleTimeout implements TimeoutPauseHandle {
68
90
  }
69
91
 
70
92
  #expire(): void {
71
- if (this.#settled || this.#pauseDepth > 0) return;
72
- const remainingMs = this.#deadlineMs - Date.now();
93
+ if (this.#settled) return;
94
+ const pausedDeadlineMs = this.#pausedDeadlineMs;
95
+ if (this.#pauseDepth > 0 && pausedDeadlineMs === undefined) return;
96
+ const deadlineMs = pausedDeadlineMs ?? this.#deadlineMs;
97
+ const remainingMs = deadlineMs - Date.now();
73
98
  if (remainingMs > 0) {
74
99
  this.#arm(remainingMs);
75
100
  return;
76
101
  }
77
102
  this.#settled = true;
103
+ this.#pausedDeadlineMs = undefined;
78
104
  this.#timer = undefined;
79
- const error = new Error(`Cell timed out after ${this.timeoutMs}ms`);
105
+ const error =
106
+ pausedDeadlineMs === undefined
107
+ ? new Error(`Cell timed out after ${this.timeoutMs}ms`)
108
+ : new Error(`Cell timed out after ${this.maxPauseGraceMs}ms waiting on a host tool call`);
80
109
  error.name = "TimeoutError";
81
110
  this.#controller.abort(error);
82
111
  this.#onTimeout({ cellId: this.#cellId, error });
@@ -0,0 +1,40 @@
1
+ # src/tool
2
+
3
+ Eval tool core: input schema, cell execution lifecycle, detached-cell state
4
+ machine, status events, and all call/result rendering. Earned by score 12 —
5
+ highest reference density in the package (`createEvalTool` and `EvalToolDetails`
6
+ anchor most suites).
7
+
8
+ ## WHERE TO LOOK
9
+
10
+ | Task | Path |
11
+ | --- | --- |
12
+ | Tool registration, options | `eval-tool.ts`, `eval-tool-options.ts`, `eval-request.ts` |
13
+ | Wire contract, TypeBox schemas | `types.ts` (`createEvalInputSchema`, `fullEvalInputSchema`) |
14
+ | Cell execution, settlement | `cell-handler.ts`, `cell-execution.ts`, `cell-runtime.ts` |
15
+ | Detached cells | `detached-cell-manager.ts` + `detached-cell-{state,snapshot,notification}.ts`, `detached-notification-queue.ts`, `detached-eval-result.ts` |
16
+ | Call/result rendering | `render.ts`, `runtime-label.ts`, `json-tree.ts`, `image.ts`, `tool-widgets.ts` |
17
+ | Status events, execution events | `status-events.ts`, `eval-execution-event.ts` |
18
+ | Interrupt, capture | `interrupt-note.ts`, `call-capture.ts` |
19
+
20
+ ## CONVENTIONS
21
+
22
+ - Wire/schema fields are snake_case (`cell_id`, `on_timeout`); internal TS
23
+ fields are camelCase. Schemas and shared eval types live only in `types.ts`.
24
+ - Rendering is bounded by explicit line/code-point budgets with an injectable
25
+ render clock; nothing here depends on wall-clock luck.
26
+ - Detached execution is a first-class state machine — snapshot, notification
27
+ queue, spill-file notice, result conversion — never folded into ordinary
28
+ cell execution.
29
+ - Unicode tree glyphs and status icons are intentional UI conventions.
30
+
31
+ ## ANTI-PATTERNS
32
+
33
+ - Never add unbounded output: previews, JSON tree depth/lines/scalar length,
34
+ widget lines, and collapsed errors all cap.
35
+ - Never bypass the kernel bridge message contract with ad-hoc return values;
36
+ kernels import `KernelInterruptHandle` from `types.ts`, so discriminants and
37
+ lifecycle state are cross-runtime contracts — change them only with all four
38
+ runtimes and the detached path in mind.
39
+ - `render.ts` (1,030 LOC) is the package's largest file and the highest-risk
40
+ hotspot for regressions; changes there need render contracts first.
@@ -2,7 +2,7 @@ import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from
2
2
  import type { KernelToHostMessage } from "../bridge/protocol.ts";
3
3
  import type { EvalToolCallMetric } from "./call-capture.ts";
4
4
  import { type EvalImageResizer, EvalOutputCollector, type EvalOutputResult } from "./image.ts";
5
- import type { EvalStatusEvent, EvalToolDetails, EvalToolInput } from "./types.ts";
5
+ import type { EvalRuntimeInfo, EvalStatusEvent, EvalToolDetails, EvalToolInput } from "./types.ts";
6
6
 
7
7
  type KernelResult = Extract<KernelToHostMessage, { type: "result" }>;
8
8
  type DisplayMessage = Extract<KernelToHostMessage, { type: "display" }>;
@@ -10,6 +10,7 @@ type ToolCall = EvalToolDetails["toolCalls"] extends readonly (infer Item)[] ? I
10
10
 
11
11
  export interface CellState {
12
12
  readonly input: EvalToolInput;
13
+ readonly runtime?: EvalRuntimeInfo;
13
14
  readonly startedAt: number;
14
15
  readonly signal: AbortSignal;
15
16
  readonly onUpdate: AgentToolUpdateCallback<EvalToolDetails> | undefined;
@@ -125,6 +126,7 @@ export class CellResultBuilder {
125
126
  return {
126
127
  language: this.#state.input.language,
127
128
  languages: [this.#state.input.language],
129
+ ...(this.#state.runtime === undefined ? {} : { runtime: this.#state.runtime }),
128
130
  ...(this.#state.input.summary === undefined ? {} : { summary: this.#state.input.summary }),
129
131
  durationMs: this.#state.durationMs,
130
132
  wallDurationMs: Math.max(0, Date.now() - this.#state.startedAt),
@@ -139,9 +141,11 @@ export class CellResultBuilder {
139
141
  ...(this.#state.input.summary === undefined ? {} : { summary: this.#state.input.summary }),
140
142
  code: this.#state.input.code,
141
143
  language: this.#state.input.language,
144
+ ...(this.#state.runtime === undefined ? {} : { runtime: this.#state.runtime }),
142
145
  output: this.#state.output,
143
146
  status: this.#state.status,
144
147
  durationMs: this.#state.durationMs,
148
+ startedAt: this.#state.startedAt,
145
149
  ...(statusEvents === undefined ? {} : { statusEvents }),
146
150
  ...(output?.hasMarkdown ? { hasMarkdown: true } : {}),
147
151
  },
@@ -93,7 +93,7 @@ export class EvalDetachedCellManager {
93
93
  this.#artifactsDir = options.artifactsDir;
94
94
  this.#onStatusChange = options.onStatusChange;
95
95
  this.#onWakeSourceState = options.onWakeSourceState;
96
- this.#notificationQueue = new DetachedNotificationQueue(options.notifier, options.artifactsDir);
96
+ this.#notificationQueue = new DetachedNotificationQueue(options.notifier);
97
97
  this.#now = options.now ?? Date.now;
98
98
  this.#hardLimitSeconds = options.hardLimitSeconds ?? DEFAULT_HARD_LIMIT_SECONDS;
99
99
  }
@@ -12,7 +12,6 @@ export function detachedNotificationSpillPath(artifactsDir: string | undefined,
12
12
  export async function buildDetachedCellNotification(
13
13
  snapshot: EvalDetachedCellSnapshot,
14
14
  spillPath: string | undefined,
15
- artifactsDir: string | undefined,
16
15
  ): Promise<EvalDetachedCellNotification> {
17
16
  const body = notificationBody(snapshot);
18
17
  const overflow = Buffer.byteLength(body, "utf8") > NOTIFICATION_TAIL_BYTES;
@@ -21,7 +20,9 @@ export async function buildDetachedCellNotification(
21
20
  try {
22
21
  await mkdir(dirname(spillPath), { recursive: true });
23
22
  await writeFile(spillPath, body, "utf8");
24
- spillNotice = `\nBuffered output overflowed; full output: ${localUri(spillPath, artifactsDir)}`;
23
+ // The agent read tool resolves plain paths only, so the notice must carry
24
+ // the absolute spill path, never the kernel-helper local:// scheme.
25
+ spillNotice = `\nBuffered output overflowed; full output: ${spillPath}`;
25
26
  } catch (error) {
26
27
  const message = error instanceof Error ? error.message : String(error);
27
28
  spillNotice = `\nBuffered output overflow could not be spilled: ${message}`;
@@ -84,12 +85,6 @@ function safeCellId(cellId: string): string {
84
85
  return cellId.replace(/[^a-zA-Z0-9_-]/gu, "_");
85
86
  }
86
87
 
87
- function localUri(path: string, artifactsDir: string | undefined): string {
88
- if (artifactsDir === undefined) return `local://${path}`;
89
- const root = join(artifactsDir, "local");
90
- return path.startsWith(`${root}/`) ? `local://${path.slice(root.length + 1)}` : `local://${path}`;
91
- }
92
-
93
88
  function truncateTailUtf8(text: string, maxBytes: number): string {
94
89
  if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
95
90
  const bytes = Buffer.from(text, "utf8");
@@ -7,14 +7,12 @@ export interface PendingDetachedNotification {
7
7
  }
8
8
 
9
9
  export class DetachedNotificationQueue {
10
- readonly #artifactsDir: string | undefined;
11
10
  readonly #notifier: EvalDetachedCellNotifier | undefined;
12
11
  #pending: PendingDetachedNotification[] = [];
13
12
  #flush: Promise<void> | undefined;
14
13
 
15
- constructor(notifier: EvalDetachedCellNotifier | undefined, artifactsDir: string | undefined) {
14
+ constructor(notifier: EvalDetachedCellNotifier | undefined) {
16
15
  this.#notifier = notifier;
17
- this.#artifactsDir = artifactsDir;
18
16
  }
19
17
 
20
18
  enqueue(notification: PendingDetachedNotification): void {
@@ -32,9 +30,7 @@ export class DetachedNotificationQueue {
32
30
  const flush = Promise.resolve().then(async () => {
33
31
  const pending = this.#pending.splice(0);
34
32
  const notifications = await Promise.all(
35
- pending.map(
36
- async (item) => await buildDetachedCellNotification(item.snapshot(), item.spillPath, this.#artifactsDir),
37
- ),
33
+ pending.map(async (item) => await buildDetachedCellNotification(item.snapshot(), item.spillPath)),
38
34
  );
39
35
  this.#notifier?.notify(notifications);
40
36
  });
@@ -11,6 +11,7 @@ import type {
11
11
  EnabledEvalLanguages,
12
12
  EvalInputSchema,
13
13
  EvalKernelManager,
14
+ EvalRuntimes,
14
15
  EvalToolDetails,
15
16
  EvalToolInput,
16
17
  ExecuteTool,
@@ -38,6 +39,8 @@ export interface CreateEvalToolOptions {
38
39
  readonly spawnDefaultAgent?: string;
39
40
  readonly modelId?: string;
40
41
  readonly hostLine?: string;
42
+ /** Display identity of each language's runtime, shown in headers and details. */
43
+ readonly runtimes?: EvalRuntimes;
41
44
  }
42
45
 
43
46
  export interface EvalCellInvocation {
@@ -98,8 +98,10 @@ async function runEvalCell(
98
98
  const bridgeAbortController = new AbortController();
99
99
  const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
100
100
  const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
101
+ const runtime = options.runtimes?.[invocation.input.language];
101
102
  const state: CellState = {
102
103
  input: invocation.input,
104
+ ...(runtime === undefined ? {} : { runtime }),
103
105
  startedAt: Date.now(),
104
106
  signal: cellSignal,
105
107
  onUpdate: invocation.onUpdate,