@pikku/core 0.12.78 → 0.12.80

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 (341) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/dist/dev/hot-reload.js +1 -6
  3. package/dist/ecosystem.d.ts +27 -0
  4. package/dist/ecosystem.js +26 -0
  5. package/dist/errors/error-handler.js +4 -2
  6. package/dist/function/function-runner.js +20 -42
  7. package/dist/function/functions.types.d.ts +9 -5
  8. package/dist/index.d.ts +4 -9
  9. package/dist/index.js +3 -8
  10. package/dist/middleware/auth-cookie.d.ts +0 -4
  11. package/dist/middleware/auth-cookie.js +38 -2
  12. package/dist/middleware/cors.js +1 -0
  13. package/dist/middleware/index.d.ts +1 -1
  14. package/dist/middleware/index.js +1 -1
  15. package/dist/middleware/remote-auth.js +14 -3
  16. package/dist/permissions.d.ts +8 -10
  17. package/dist/permissions.js +0 -11
  18. package/dist/pikku-state.js +12 -5
  19. package/dist/schema.js +35 -1
  20. package/dist/services/ai-run-state-service.d.ts +7 -1
  21. package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
  22. package/dist/services/in-memory-ai-run-state-service.js +2 -1
  23. package/dist/services/in-memory-workflow-service.d.ts +10 -0
  24. package/dist/services/in-memory-workflow-service.js +25 -0
  25. package/dist/services/local-content-request-handler.d.ts +21 -0
  26. package/dist/services/local-content-request-handler.js +72 -53
  27. package/dist/services/local-content.d.ts +6 -0
  28. package/dist/services/local-content.js +14 -1
  29. package/dist/services/workflow-service.d.ts +4 -2
  30. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  31. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  32. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  33. package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
  34. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  35. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  36. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  37. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  38. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  39. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  40. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  41. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  42. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  43. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  44. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  45. package/dist/testing/service-tests/session-store-tests.js +43 -0
  46. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  47. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  48. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  49. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  50. package/dist/testing/service-tests.d.ts +6 -0
  51. package/dist/testing/service-tests.js +26 -791
  52. package/dist/types/core.types.d.ts +11 -1
  53. package/dist/types/state.types.d.ts +1 -1
  54. package/dist/utils/node-host-resolver.d.ts +12 -0
  55. package/dist/utils/node-host-resolver.js +16 -0
  56. package/dist/utils/safe-fetch.d.ts +18 -0
  57. package/dist/utils/safe-fetch.js +167 -29
  58. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  59. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  60. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  61. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  62. package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
  63. package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
  64. package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
  65. package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
  66. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
  67. package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
  68. package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
  69. package/dist/wirings/ai-agent/voice-input.js +1 -6
  70. package/dist/wirings/ai-agent/voice-output.js +2 -12
  71. package/dist/wirings/channel/channel-common.js +1 -0
  72. package/dist/wirings/channel/channel-handler.js +3 -5
  73. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  74. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  75. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  76. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  77. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  78. package/dist/wirings/channel/channel-runner.js +16 -8
  79. package/dist/wirings/channel/channel.types.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  82. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  83. package/dist/wirings/cli/cli-runner.js +4 -2
  84. package/dist/wirings/cli/cli.types.d.ts +0 -8
  85. package/dist/wirings/cli/command-parser.js +13 -0
  86. package/dist/wirings/gateway/gateway-runner.js +41 -5
  87. package/dist/wirings/http/http-routes.js +2 -0
  88. package/dist/wirings/http/http-runner.d.ts +0 -10
  89. package/dist/wirings/http/http-runner.js +6 -13
  90. package/dist/wirings/http/http.types.d.ts +0 -10
  91. package/dist/wirings/http/index.d.ts +1 -1
  92. package/dist/wirings/http/index.js +1 -1
  93. package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
  94. package/dist/wirings/mcp/mcp-runner.js +0 -6
  95. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  96. package/dist/wirings/rpc/rpc-runner.js +53 -86
  97. package/dist/wirings/rpc/rpc-types.d.ts +3 -0
  98. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  99. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  100. package/dist/wirings/trigger/trigger-runner.js +1 -0
  101. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  102. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  103. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  104. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
  105. package/dist/wirings/workflow/feature.js +7 -4
  106. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  107. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  108. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  109. package/dist/wirings/workflow/index.d.ts +7 -2
  110. package/dist/wirings/workflow/index.js +5 -1
  111. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  112. package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
  114. package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
  115. package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
  116. package/dist/wirings/workflow/workflow-approval.js +114 -0
  117. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  118. package/dist/wirings/workflow/workflow-constants.js +35 -0
  119. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  120. package/dist/wirings/workflow/workflow-errors.js +112 -0
  121. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  122. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  123. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  124. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  125. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  126. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  127. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  128. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  129. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  130. package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
  131. package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
  132. package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
  133. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  134. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  135. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  136. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  137. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  138. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  139. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  140. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  141. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  142. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  143. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  144. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  145. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  146. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  147. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  148. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  149. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  150. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  151. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  152. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  153. package/knowledge/decisions/internals/index.md +38 -26
  154. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  155. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  156. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  157. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  158. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  159. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  160. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  161. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  162. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  163. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  164. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  165. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  166. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  167. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  168. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  169. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  170. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  171. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  172. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  173. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  174. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  175. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  176. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  177. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
  178. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  179. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  180. package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
  181. package/knowledge/decisions/security/index.md +7 -0
  182. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  183. package/knowledge/questions/index.md +2 -1
  184. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  185. package/package.json +14 -3
  186. package/scripts/generate-api-report.d.mts +1 -0
  187. package/scripts/generate-api-report.mjs +89 -0
  188. package/scripts/generate-api-report.mts +218 -0
  189. package/src/api-report.test.ts +69 -0
  190. package/src/column-form.ts +1 -4
  191. package/src/crypto-utils.test.ts +15 -2
  192. package/src/dev/hot-reload.ts +2 -7
  193. package/src/ecosystem.ts +40 -0
  194. package/src/errors/error-handler.ts +5 -3
  195. package/src/errors/error.test.ts +4 -1
  196. package/src/function/function-runner.test.ts +1 -1
  197. package/src/function/function-runner.ts +28 -39
  198. package/src/function/functions.types.ts +15 -9
  199. package/src/handle-error.ts +1 -1
  200. package/src/index.ts +3 -22
  201. package/src/middleware/auth-cookie.test.ts +50 -0
  202. package/src/middleware/auth-cookie.ts +38 -2
  203. package/src/middleware/cors.ts +2 -1
  204. package/src/middleware/index.ts +1 -1
  205. package/src/middleware/remote-auth.test.ts +43 -0
  206. package/src/middleware/remote-auth.ts +17 -3
  207. package/src/middleware-runner.ts +4 -2
  208. package/src/no-any-casts.test.ts +57 -0
  209. package/src/permissions.test.ts +3 -1
  210. package/src/permissions.ts +22 -24
  211. package/src/pikku-state.ts +17 -6
  212. package/src/public-surface.json +578 -0
  213. package/src/public-surface.json.README +25 -0
  214. package/src/public-surface.test.ts +105 -0
  215. package/src/removed-legacy-exports.test.ts +62 -0
  216. package/src/schema.test.ts +78 -0
  217. package/src/schema.ts +36 -1
  218. package/src/services/ai-run-state-service.ts +7 -1
  219. package/src/services/audit-service.ts +2 -2
  220. package/src/services/in-memory-ai-run-state-service.ts +3 -2
  221. package/src/services/in-memory-workflow-service.ts +25 -0
  222. package/src/services/index.ts +1 -4
  223. package/src/services/local-content-request-handler.test.ts +43 -5
  224. package/src/services/local-content-request-handler.ts +103 -74
  225. package/src/services/local-content.ts +15 -1
  226. package/src/services/local-email-service.ts +5 -1
  227. package/src/services/system-role-guard.test.ts +4 -1
  228. package/src/services/workflow-service.ts +9 -2
  229. package/src/side-effects-are-declared.test.ts +84 -0
  230. package/src/source-files-stay-composable.test.ts +41 -0
  231. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  232. package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
  233. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  234. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  235. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  236. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  237. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  238. package/src/testing/service-tests/session-store-tests.ts +62 -0
  239. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  240. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  241. package/src/testing/service-tests.ts +31 -1111
  242. package/src/types/core.types.ts +15 -1
  243. package/src/types/state.types.ts +1 -1
  244. package/src/utils/node-host-resolver.ts +20 -0
  245. package/src/utils/safe-fetch.test.ts +143 -1
  246. package/src/utils/safe-fetch.ts +200 -25
  247. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  248. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  249. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  250. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  251. package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
  252. package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
  253. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  254. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  255. package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
  256. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  257. package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
  258. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  259. package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
  260. package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
  261. package/src/wirings/ai-agent/voice-input.ts +1 -6
  262. package/src/wirings/ai-agent/voice-output.ts +2 -12
  263. package/src/wirings/channel/channel-common.ts +5 -2
  264. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  265. package/src/wirings/channel/channel-handler.ts +7 -7
  266. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  267. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  268. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  269. package/src/wirings/channel/channel-runner.ts +33 -20
  270. package/src/wirings/channel/channel.types.ts +2 -0
  271. package/src/wirings/channel/local/local-channel-runner.ts +1 -1
  272. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  273. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  274. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  275. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  276. package/src/wirings/cli/cli-runner.ts +7 -4
  277. package/src/wirings/cli/cli.types.ts +0 -39
  278. package/src/wirings/cli/command-parser.test.ts +19 -0
  279. package/src/wirings/cli/command-parser.ts +17 -1
  280. package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
  281. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  282. package/src/wirings/gateway/gateway-runner.ts +62 -22
  283. package/src/wirings/http/http-routes.ts +4 -1
  284. package/src/wirings/http/http-runner.ts +11 -26
  285. package/src/wirings/http/http.types.ts +0 -15
  286. package/src/wirings/http/index.ts +1 -7
  287. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  288. package/src/wirings/http/web-request.ts +1 -1
  289. package/src/wirings/mcp/mcp-runner.ts +2 -16
  290. package/src/wirings/persona/persona-environments.test.ts +14 -3
  291. package/src/wirings/persona/persona.test.ts +13 -3
  292. package/src/wirings/persona/validate-personas.ts +5 -1
  293. package/src/wirings/queue/queue-runner.ts +1 -1
  294. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  295. package/src/wirings/rpc/rpc-runner.ts +58 -136
  296. package/src/wirings/rpc/rpc-types.ts +7 -0
  297. package/src/wirings/rpc/wire-addon.ts +3 -1
  298. package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
  299. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  300. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  301. package/src/wirings/trigger/trigger-runner.ts +7 -5
  302. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  303. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  304. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  305. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  306. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  307. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  308. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  309. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
  310. package/src/wirings/workflow/feature.ts +7 -4
  311. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  312. package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
  313. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  314. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  315. package/src/wirings/workflow/index.ts +12 -4
  316. package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
  317. package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
  318. package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
  319. package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
  320. package/src/wirings/workflow/workflow-approval.ts +185 -0
  321. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  322. package/src/wirings/workflow/workflow-constants.ts +47 -0
  323. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  324. package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
  325. package/src/wirings/workflow/workflow-errors.ts +128 -0
  326. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  327. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  328. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  329. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  330. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  331. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  332. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  333. package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
  334. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  335. package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
  336. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  337. package/src/wirings/workflow/workflow.types.ts +7 -0
  338. package/src/wirings-stay-decoupled.test.ts +123 -0
  339. package/tsconfig.json +1 -1
  340. package/tsconfig.tsbuildinfo +1 -1
  341. package/src/internal.ts +0 -10
@@ -233,6 +233,11 @@ export interface CoreUserSession {
233
233
  * Populated by whoever builds the session — core reads them, never fetches.
234
234
  */
235
235
  scopes?: string[];
236
+ /**
237
+ * Restricts the session to functions declared `readonly`. The function runner
238
+ * throws `ReadonlySessionError` for anything else.
239
+ */
240
+ readonly?: boolean;
236
241
  }
237
242
  /**
238
243
  * Kept structural so core stays independent of any one auth package —
@@ -299,12 +304,17 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
299
304
  */
300
305
  auth?: () => Promise<AuthInstance>;
301
306
  }
302
- export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boolean = false, UserSession extends CoreUserSession = CoreUserSession, TypedRPC extends PikkuRPC = PikkuRPC, IsChannel extends true | null = null, MCPTools extends string | never = never, TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire, TriggerOutput = unknown, TypedScenario extends PikkuScenarioWire | never = PikkuScenarioWire, TypedActors extends ScenarioPersonas = ScenarioPersonas> = {
307
+ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boolean = false, UserSession extends CoreUserSession = CoreUserSession, TypedRPC extends PikkuRPC = PikkuRPC, IsChannel extends true | null = null, MCPTools extends string | never = never, TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire, TriggerOutput = unknown, TypedScenario extends PikkuScenarioWire<any> | never = PikkuScenarioWire<any>, TypedActors extends ScenarioPersonas = ScenarioPersonas> = {
303
308
  /** Always present — lazily initialised on first access for every function invocation */
304
309
  rpc: TypedRPC;
305
310
  } & Partial<{
306
311
  wireType: PikkuWiringTypes;
307
312
  wireId: string;
313
+ /**
314
+ * A logger scoped to this invocation, when a host attaches one. Core never
315
+ * sets it; services that log fall back to the singleton logger.
316
+ */
317
+ logger: Logger;
308
318
  /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
309
319
  traceId: string;
310
320
  functionId: string;
@@ -63,7 +63,7 @@ export interface PikkuPackageState {
63
63
  meta: HTTPWiringsMeta;
64
64
  };
65
65
  channel: {
66
- channels: Map<string, CoreChannel<any, any>>;
66
+ channels: Map<string, CoreChannel<any, any, any, any, any>>;
67
67
  meta: ChannelsMeta;
68
68
  };
69
69
  scheduler: {
@@ -0,0 +1,12 @@
1
+ import { type HostResolver } from './safe-fetch.js';
2
+ /**
3
+ * Resolves a hostname through the platform resolver, returning every address it
4
+ * points at so `safeFetch` can reject a public name aimed at an internal one.
5
+ *
6
+ * This module is Node-only and is never imported by core itself — Workers has no
7
+ * DNS API, and a static `node:dns` import in the shared path would break that
8
+ * build.
9
+ */
10
+ export declare const nodeHostResolver: HostResolver;
11
+ /** Installs {@link nodeHostResolver} as the default for every `safeFetch`. */
12
+ export declare const installNodeHostResolver: () => void;
@@ -0,0 +1,16 @@
1
+ import { lookup } from 'node:dns/promises';
2
+ import { setDefaultHostResolver } from './safe-fetch.js';
3
+ /**
4
+ * Resolves a hostname through the platform resolver, returning every address it
5
+ * points at so `safeFetch` can reject a public name aimed at an internal one.
6
+ *
7
+ * This module is Node-only and is never imported by core itself — Workers has no
8
+ * DNS API, and a static `node:dns` import in the shared path would break that
9
+ * build.
10
+ */
11
+ export const nodeHostResolver = async (hostname) => {
12
+ const results = await lookup(hostname, { all: true, verbatim: true });
13
+ return results.map(({ address }) => address);
14
+ };
15
+ /** Installs {@link nodeHostResolver} as the default for every `safeFetch`. */
16
+ export const installNodeHostResolver = () => setDefaultHostResolver(nodeHostResolver);
@@ -4,6 +4,18 @@
4
4
  * private IP.
5
5
  */
6
6
  export declare function isPrivateHost(hostname: string): boolean;
7
+ /** Resolves a hostname to the IP addresses it points at. */
8
+ export type HostResolver = (hostname: string) => Promise<string[]>;
9
+ /**
10
+ * Installs the resolver {@link safeFetch} uses when a call passes no
11
+ * `resolveHost` of its own.
12
+ *
13
+ * Core cannot resolve DNS itself — Workers has no DNS API — so without a
14
+ * resolver the guard is literal-only and a public name pointing at
15
+ * `169.254.169.254` passes. Node runtimes install
16
+ * `nodeHostResolver` from `@pikku/core/node-host-resolver` at startup.
17
+ */
18
+ export declare function setDefaultHostResolver(resolver: HostResolver | undefined): void;
7
19
  export interface SafeFetchOptions {
8
20
  /**
9
21
  * When set, the host of every hop must appear in this allowlist. When omitted,
@@ -12,6 +24,12 @@ export interface SafeFetchOptions {
12
24
  allowedHosts?: string[];
13
25
  /** Maximum redirect hops to follow (each re-validated). Defaults to 3. */
14
26
  maxRedirects?: number;
27
+ /**
28
+ * Resolves a hostname so a *public* name pointing at a private address is
29
+ * refused. Defaults to whatever {@link setDefaultHostResolver} installed;
30
+ * pass `null` to opt a call out of resolution entirely.
31
+ */
32
+ resolveHost?: HostResolver | null;
15
33
  }
16
34
  export declare function assertFetchableUrl(url: string, options?: SafeFetchOptions): URL;
17
35
  /**
@@ -34,6 +34,120 @@ function parseIPv4Octets(host) {
34
34
  }
35
35
  return octets;
36
36
  }
37
+ /**
38
+ * IPv4 blocks that must never be reachable from user-supplied URLs: private,
39
+ * loopback, link-local (cloud metadata), carrier-grade NAT (Alibaba's
40
+ * `100.100.100.200` metadata endpoint), IETF protocol assignments, benchmarking,
41
+ * 6to4 anycast, the documentation TEST-NETs, multicast and reserved space.
42
+ */
43
+ const PRIVATE_IPV4_BLOCKS = [
44
+ ['0.0.0.0', 8],
45
+ ['10.0.0.0', 8],
46
+ ['100.64.0.0', 10],
47
+ ['127.0.0.0', 8],
48
+ ['169.254.0.0', 16],
49
+ ['172.16.0.0', 12],
50
+ ['192.0.0.0', 24],
51
+ ['192.0.2.0', 24],
52
+ ['192.88.99.0', 24],
53
+ ['192.168.0.0', 16],
54
+ ['198.18.0.0', 15],
55
+ ['198.51.100.0', 24],
56
+ ['203.0.113.0', 24],
57
+ ['224.0.0.0', 4],
58
+ ['240.0.0.0', 4],
59
+ ];
60
+ const toUint32 = (octets) => ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
61
+ const PRIVATE_IPV4_RANGES = PRIVATE_IPV4_BLOCKS.map(([base, prefix]) => {
62
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
63
+ return [(toUint32(parseIPv4Octets(base)) & mask) >>> 0, mask];
64
+ });
65
+ const isPrivateIPv4 = (octets) => {
66
+ const addr = toUint32(octets);
67
+ return PRIVATE_IPV4_RANGES.some(([base, mask]) => (addr & mask) >>> 0 === base);
68
+ };
69
+ /**
70
+ * Expands an IPv6 literal into its eight 16-bit groups, handling `::` elision
71
+ * and a trailing dotted-quad. `null` when not a well-formed IPv6 literal.
72
+ */
73
+ function parseIPv6Groups(host) {
74
+ const zoneless = host.split('%')[0];
75
+ const halves = zoneless.split('::');
76
+ if (halves.length > 2)
77
+ return null;
78
+ const parseSide = (side) => {
79
+ if (side === '')
80
+ return [];
81
+ const parts = side.split(':');
82
+ const groups = [];
83
+ for (let i = 0; i < parts.length; i++) {
84
+ const part = parts[i];
85
+ if (i === parts.length - 1 && part.includes('.')) {
86
+ const v4 = parseIPv4Octets(part);
87
+ if (!v4)
88
+ return null;
89
+ groups.push((v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3]);
90
+ continue;
91
+ }
92
+ if (!/^[0-9a-f]{1,4}$/.test(part))
93
+ return null;
94
+ groups.push(parseInt(part, 16));
95
+ }
96
+ return groups;
97
+ };
98
+ const head = parseSide(halves[0]);
99
+ if (head === null)
100
+ return null;
101
+ if (halves.length === 1) {
102
+ return head.length === 8 ? head : null;
103
+ }
104
+ const tail = parseSide(halves[1]);
105
+ if (tail === null)
106
+ return null;
107
+ if (head.length + tail.length > 7)
108
+ return null;
109
+ return [...head, ...new Array(8 - head.length - tail.length).fill(0), ...tail];
110
+ }
111
+ const embeddedIPv4 = (hi, lo) => [
112
+ (hi >> 8) & 0xff,
113
+ hi & 0xff,
114
+ (lo >> 8) & 0xff,
115
+ lo & 0xff,
116
+ ];
117
+ function isPrivateIPv6(groups) {
118
+ const [g0, g1, g2, g3, g4, g5, g6, g7] = groups;
119
+ if (groups.every((g) => g === 0))
120
+ return true; // :: unspecified
121
+ if (groups.slice(0, 7).every((g) => g === 0) && g7 === 1)
122
+ return true; // ::1
123
+ // IPv4-mapped ::ffff:0:0/96 and IPv4-compatible ::/96
124
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0) {
125
+ if (g5 === 0xffff || g5 === 0)
126
+ return isPrivateIPv4(embeddedIPv4(g6, g7));
127
+ }
128
+ // NAT64 well-known prefix 64:ff9b::/96
129
+ if (g0 === 0x64 &&
130
+ g1 === 0xff9b &&
131
+ g2 === 0 &&
132
+ g3 === 0 &&
133
+ g4 === 0 &&
134
+ g5 === 0)
135
+ return isPrivateIPv4(embeddedIPv4(g6, g7));
136
+ // 6to4 2002::/16 carries the IPv4 address in the next 32 bits
137
+ if (g0 === 0x2002)
138
+ return isPrivateIPv4(embeddedIPv4(g1, g2));
139
+ if (g0 === 0x100 && g1 === 0 && g2 === 0 && g3 === 0)
140
+ return true; // discard-only 100::/64
141
+ if ((g0 & 0xffc0) === 0xfe80)
142
+ return true; // link-local fe80::/10
143
+ if ((g0 & 0xfe00) === 0xfc00)
144
+ return true; // unique-local fc00::/7
145
+ if ((g0 & 0xffc0) === 0xfec0)
146
+ return true; // deprecated site-local fec0::/10
147
+ if ((g0 & 0xff00) === 0xff00)
148
+ return true; // multicast ff00::/8
149
+ return false;
150
+ }
37
151
  /**
38
152
  * Whether a hostname is an obvious internal target. Best-effort literal
39
153
  * matching only: it cannot catch a public hostname that *resolves* to a
@@ -47,36 +161,56 @@ export function isPrivateHost(hostname) {
47
161
  if (host === '' || host === 'localhost' || host.endsWith('.localhost'))
48
162
  return true;
49
163
  if (host.includes(':')) {
50
- if (host === '::' || host === '::1')
51
- return true;
52
- const mappedV4 = host.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
53
- if (mappedV4)
54
- return isPrivateHost(mappedV4[1]);
55
- const mappedHex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
56
- if (mappedHex) {
57
- const hi = parseInt(mappedHex[1], 16);
58
- const lo = parseInt(mappedHex[2], 16);
59
- return isPrivateHost(`${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`);
60
- }
61
- if (/^fe[89ab]/.test(host))
62
- return true; // link-local fe80::/10
63
- if (host.startsWith('fc') || host.startsWith('fd'))
64
- return true; // unique-local fc00::/7
65
- return false;
164
+ const groups = parseIPv6Groups(host);
165
+ return groups ? isPrivateIPv6(groups) : false;
66
166
  }
67
167
  const v4 = parseIPv4Octets(host);
68
- if (v4) {
69
- const [a, b] = v4;
70
- if (a === 127 || a === 10 || a === 0)
71
- return true;
72
- if (a === 169 && b === 254)
73
- return true; // link-local incl. cloud metadata
74
- if (a === 172 && b >= 16 && b <= 31)
75
- return true;
76
- if (a === 192 && b === 168)
77
- return true;
168
+ return v4 ? isPrivateIPv4(v4) : false;
169
+ }
170
+ let defaultHostResolver;
171
+ /**
172
+ * Installs the resolver {@link safeFetch} uses when a call passes no
173
+ * `resolveHost` of its own.
174
+ *
175
+ * Core cannot resolve DNS itself — Workers has no DNS API — so without a
176
+ * resolver the guard is literal-only and a public name pointing at
177
+ * `169.254.169.254` passes. Node runtimes install
178
+ * `nodeHostResolver` from `@pikku/core/node-host-resolver` at startup.
179
+ */
180
+ export function setDefaultHostResolver(resolver) {
181
+ defaultHostResolver = resolver;
182
+ }
183
+ /** Whether a hostname is already an IP literal, which the sync check covers. */
184
+ function isIpLiteral(hostname) {
185
+ const host = hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '');
186
+ if (host.includes(':'))
187
+ return parseIPv6Groups(host.toLowerCase()) !== null;
188
+ return parseIPv4Octets(host) !== null;
189
+ }
190
+ /**
191
+ * Rejects a hostname that resolves to an internal address.
192
+ *
193
+ * Resolution happens once per hop and the connection is not pinned to the
194
+ * address checked, so a rebind between this check and the socket connecting is
195
+ * still possible; catching that needs a runtime-level connect hook.
196
+ */
197
+ async function assertResolvedHostAllowed(hostname, options) {
198
+ if (options.allowedHosts)
199
+ return;
200
+ const resolver = options.resolveHost === null
201
+ ? undefined
202
+ : (options.resolveHost ?? defaultHostResolver);
203
+ if (!resolver || isIpLiteral(hostname))
204
+ return;
205
+ const addresses = await resolver(hostname);
206
+ if (addresses.length === 0) {
207
+ throw new Error(`Refusing to fetch: '${hostname}' resolved to no addresses`);
208
+ }
209
+ for (const address of addresses) {
210
+ if (isPrivateHost(address)) {
211
+ throw new Error(`Refusing to fetch from a private/internal host: '${hostname}' resolves to ${address}`);
212
+ }
78
213
  }
79
- return false;
80
214
  }
81
215
  export function assertFetchableUrl(url, options = {}) {
82
216
  const parsed = new URL(url);
@@ -126,7 +260,9 @@ function redirectInit(status, init) {
126
260
  */
127
261
  export async function safeFetch(url, init = {}, options = {}) {
128
262
  const maxRedirects = options.maxRedirects ?? 3;
129
- let currentUrl = assertFetchableUrl(url, options).toString();
263
+ const initial = assertFetchableUrl(url, options);
264
+ await assertResolvedHostAllowed(initial.hostname, options);
265
+ let currentUrl = initial.toString();
130
266
  let currentInit = init;
131
267
  for (let hop = 0;; hop++) {
132
268
  const response = await fetch(currentUrl, {
@@ -140,7 +276,9 @@ export async function safeFetch(url, init = {}, options = {}) {
140
276
  if (!location || hop >= maxRedirects) {
141
277
  return response;
142
278
  }
143
- const nextUrl = assertFetchableUrl(new URL(location, currentUrl).toString(), options).toString();
279
+ const next = assertFetchableUrl(new URL(location, currentUrl).toString(), options);
280
+ await assertResolvedHostAllowed(next.hostname, options);
281
+ const nextUrl = next.toString();
144
282
  await response.body?.cancel();
145
283
  let nextInit = redirectInit(response.status, currentInit);
146
284
  if (new URL(nextUrl).origin !== new URL(currentUrl).origin) {
@@ -70,12 +70,7 @@ function actorInstructions(persona, task) {
70
70
  : '',
71
71
  `Your goal in this conversation: ${task}.`,
72
72
  `Send one message at a time. Set "done" to true only once your goal is clearly accomplished, or clearly impossible.`,
73
- // Every call the actor makes wants a schema'd object back, and a gateway
74
- // that cannot take a JSON *schema* degrades to OpenAI's `json_object`
75
- // mode — which refuses the request outright unless the word "json" appears
76
- // somewhere in the prompt. Saying it once here covers all three call sites
77
- // (turn, approvals, verdict), since each builds on these instructions, and
78
- // it costs nothing on providers that never needed telling.
73
+ // knowledge: decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md
79
74
  `Reply with json matching the schema you are given, and nothing else.`,
80
75
  ]
81
76
  .filter(Boolean)
@@ -0,0 +1,15 @@
1
+ import type { PikkuRawWire } from '../../types/core.types.js';
2
+ import type { SessionService } from '../../services/user-session-service.js';
3
+ import type { CoreUserSession } from '../../types/core.types.js';
4
+ import type { PikkuRPC } from '../rpc/rpc-types.js';
5
+ export type AgentRPCOptions = {
6
+ sessionService?: SessionService<CoreUserSession>;
7
+ };
8
+ /**
9
+ * `wire.rpc.agent`, implemented.
10
+ *
11
+ * Lives here rather than in `rpc-runner` so the agent surface is one file next
12
+ * to the runner and stream code it delegates to, instead of a wing of the RPC
13
+ * primitive.
14
+ */
15
+ export declare const createAgentRPC: (wire: PikkuRawWire, options: AgentRPCOptions) => PikkuRPC["agent"];
@@ -0,0 +1,53 @@
1
+ import { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
2
+ import { streamAIAgent, resumeAIAgent, interruptAIAgent, } from './ai-agent-stream.js';
3
+ import { wrapChannelWithAGUI } from './ai-agent-agui.js';
4
+ /**
5
+ * `wire.rpc.agent`, implemented.
6
+ *
7
+ * Lives here rather than in `rpc-runner` so the agent surface is one file next
8
+ * to the runner and stream code it delegates to, instead of a wing of the RPC
9
+ * primitive.
10
+ */
11
+ export const createAgentRPC = (wire, options) => {
12
+ const params = () => ({
13
+ sessionService: options.sessionService,
14
+ getCredential: wire.getCredential?.bind(wire),
15
+ });
16
+ const streamingChannel = () => {
17
+ const channel = wire.channel;
18
+ if (!channel)
19
+ throw new Error('No channel available for streaming');
20
+ return channel;
21
+ };
22
+ /** `run` and `approve` return the same shape; only the call differs. */
23
+ const asRunResult = (result) => ({
24
+ runId: result.runId,
25
+ result: result.object ?? result.text,
26
+ usage: result.usage,
27
+ ...(result.status === 'suspended' && {
28
+ status: 'suspended',
29
+ pendingApprovals: result.pendingApprovals,
30
+ }),
31
+ });
32
+ return {
33
+ run: async (agentName, input) => asRunResult(await runAIAgent(agentName, input, params())),
34
+ stream: async (agentName, input, streamOptions) => {
35
+ let currentRunId;
36
+ await streamAIAgent(agentName, input, wrapChannelWithAGUI(streamingChannel(), {
37
+ threadId: input.threadId,
38
+ getRunId: () => currentRunId,
39
+ }), params(), undefined, {
40
+ ...streamOptions,
41
+ onRunCreated: (runId) => {
42
+ currentRunId = runId;
43
+ streamOptions?.onRunCreated?.(runId);
44
+ },
45
+ });
46
+ },
47
+ resume: async (runId, input, streamOptions) => {
48
+ await resumeAIAgent({ runId, ...input }, wrapChannelWithAGUI(streamingChannel(), { runId }), params(), streamOptions);
49
+ },
50
+ interrupt: async (runId, reason) => interruptAIAgent({ runId, ...(reason ? { reason } : {}) }, { sessionService: options.sessionService }),
51
+ approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAIAgentSync(runId, approvals, { sessionService: options.sessionService }, expectedAgentName)),
52
+ };
53
+ };
@@ -296,11 +296,7 @@ export function wrapChannelWithAGUI(inner, options) {
296
296
  });
297
297
  break;
298
298
  }
299
- // AG-UI has no event for speech, so it travels as CUSTOM like the other
300
- // pikku-specific ones. Dropping it instead — which is what this did —
301
- // means a voice agent reached over HTTP is inaudible: `voiceOutput`
302
- // synthesizes every sentence, the provider bills for it, and nothing
303
- // gets past the mapper.
299
+ // knowledge: decisions/internals/agent-speech-travels-as-a-custom-agui-event.md
304
300
  case 'audio-delta': {
305
301
  send({
306
302
  type: 'CUSTOM',
@@ -2,7 +2,9 @@ import { randomUUID } from 'crypto';
2
2
  export function resolveMemoryServices(agent, singletonServices) {
3
3
  const memoryConfig = agent.memory;
4
4
  const storage = memoryConfig?.storage
5
- ? singletonServices[memoryConfig.storage]
5
+ ? // The service is named by string in agent config, so the lookup cannot be
6
+ // checked — only the shape it is required to have.
7
+ singletonServices[memoryConfig.storage]
6
8
  : singletonServices.aiStorage;
7
9
  return { storage };
8
10
  }
@@ -1,5 +1,5 @@
1
1
  import { PikkuError } from '../../errors/error-handler.js';
2
- import { checkAuthPermissions, runPermissions } from '../../permissions.js';
2
+ import { checkAuthPermissions, runPermissions, } from '../../permissions.js';
3
3
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
4
4
  import { ForbiddenError } from '../../errors/errors.js';
5
5
  import { verifyScopes } from '../../scopes.js';
@@ -648,10 +648,7 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
648
648
  }
649
649
  let messages = [];
650
650
  if (storage) {
651
- // A tool whose run was interrupted may still be writing its result to this
652
- // thread. In voice the next turn lands within a second or two, so without
653
- // this the model would load context that is missing the very thing it is
654
- // about to be asked about.
651
+ // knowledge: decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md
655
652
  await awaitPendingInterruptNote(threadId);
656
653
  messages = await storage.getMessages(threadId, {
657
654
  lastN: memoryConfig?.lastMessages ?? 20,
@@ -660,10 +657,7 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
660
657
  const contextMessages = await loadContextMessages(memoryConfig, storage, input, workingMemoryJsonSchema);
661
658
  const userContent = input.attachments?.length
662
659
  ? [
663
- // Omitted when there is nothing to say. An attachment on its own is a
664
- // real turn — a spoken one carries audio and no text at all — and an
665
- // empty text part alongside it is a part providers are entitled to
666
- // reject, for a caller who never wrote one.
660
+ // knowledge: decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md
667
661
  ...(input.message
668
662
  ? [{ type: 'text', text: input.message }]
669
663
  : []),