@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
@@ -298,6 +298,11 @@ export interface CoreUserSession {
298
298
  * Populated by whoever builds the session — core reads them, never fetches.
299
299
  */
300
300
  scopes?: string[]
301
+ /**
302
+ * Restricts the session to functions declared `readonly`. The function runner
303
+ * throws `ReadonlySessionError` for anything else.
304
+ */
305
+ readonly?: boolean
301
306
  }
302
307
 
303
308
  /**
@@ -378,7 +383,11 @@ export type PikkuWire<
378
383
  MCPTools extends string | never = never,
379
384
  TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire,
380
385
  TriggerOutput = unknown,
381
- TypedScenario extends PikkuScenarioWire | never = PikkuScenarioWire,
386
+ // Defaulted to `any` rather than to `Out`: the emitted `TypedScenario<Out>`
387
+ // supplies the real context, while `PikkuWire`'s own defaults are what other
388
+ // generics constrain against — and a constraint that pinned the context to
389
+ // `Out` would reject every wire whose scenario output is anything else.
390
+ TypedScenario extends PikkuScenarioWire<any> | never = PikkuScenarioWire<any>,
382
391
  TypedActors extends ScenarioPersonas = ScenarioPersonas,
383
392
  > = {
384
393
  /** Always present — lazily initialised on first access for every function invocation */
@@ -386,6 +395,11 @@ export type PikkuWire<
386
395
  } & Partial<{
387
396
  wireType: PikkuWiringTypes
388
397
  wireId: string
398
+ /**
399
+ * A logger scoped to this invocation, when a host attaches one. Core never
400
+ * sets it; services that log fall back to the singleton logger.
401
+ */
402
+ logger: Logger
389
403
  /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
390
404
  traceId: string
391
405
  functionId: string
@@ -119,7 +119,7 @@ export interface PikkuPackageState {
119
119
  meta: HTTPWiringsMeta
120
120
  }
121
121
  channel: {
122
- channels: Map<string, CoreChannel<any, any>>
122
+ channels: Map<string, CoreChannel<any, any, any, any, any>>
123
123
  meta: ChannelsMeta
124
124
  }
125
125
  scheduler: {
@@ -0,0 +1,20 @@
1
+ import { lookup } from 'node:dns/promises'
2
+
3
+ import { setDefaultHostResolver, type HostResolver } from './safe-fetch.js'
4
+
5
+ /**
6
+ * Resolves a hostname through the platform resolver, returning every address it
7
+ * points at so `safeFetch` can reject a public name aimed at an internal one.
8
+ *
9
+ * This module is Node-only and is never imported by core itself — Workers has no
10
+ * DNS API, and a static `node:dns` import in the shared path would break that
11
+ * build.
12
+ */
13
+ export const nodeHostResolver: HostResolver = async (hostname) => {
14
+ const results = await lookup(hostname, { all: true, verbatim: true })
15
+ return results.map(({ address }) => address)
16
+ }
17
+
18
+ /** Installs {@link nodeHostResolver} as the default for every `safeFetch`. */
19
+ export const installNodeHostResolver = () =>
20
+ setDefaultHostResolver(nodeHostResolver)
@@ -41,15 +41,54 @@ describe('isPrivateHost', () => {
41
41
  }
42
42
  })
43
43
 
44
+ test('flags reserved IPv4 blocks that are not routable public space', () => {
45
+ for (const host of [
46
+ '100.64.0.1', // carrier-grade NAT
47
+ '100.100.100.200', // Alibaba Cloud metadata endpoint
48
+ '100.127.255.255', // top of 100.64.0.0/10
49
+ '192.0.0.192', // IETF protocol assignments
50
+ '198.18.0.1', // benchmarking
51
+ '198.19.255.255', // top of 198.18.0.0/15
52
+ '192.88.99.1', // 6to4 anycast
53
+ '192.0.2.1', // TEST-NET-1
54
+ '198.51.100.1', // TEST-NET-2
55
+ '203.0.113.1', // TEST-NET-3
56
+ '224.0.0.1', // multicast
57
+ '240.0.0.1', // reserved
58
+ '255.255.255.255', // broadcast
59
+ ]) {
60
+ assert.equal(isPrivateHost(host), true, `${host} should be private`)
61
+ }
62
+ })
63
+
64
+ test('flags IPv6 forms that tunnel to an internal IPv4 address', () => {
65
+ for (const host of [
66
+ '64:ff9b::a9fe:a9fe', // NAT64 wrapping 169.254.169.254
67
+ '[64:ff9b::169.254.169.254]', // same, dotted-quad tail
68
+ '2002:a9fe:a9fe::', // 6to4 wrapping 169.254.169.254
69
+ '2002:6464:64c8::', // 6to4 wrapping 100.100.100.200
70
+ 'fec0::1', // deprecated site-local
71
+ 'ff02::1', // multicast
72
+ '100::1', // discard-only
73
+ ]) {
74
+ assert.equal(isPrivateHost(host), true, `${host} should be private`)
75
+ }
76
+ })
77
+
44
78
  test('allows public hosts', () => {
45
79
  for (const host of [
46
80
  'example.com',
47
81
  '8.8.8.8',
48
82
  '172.32.0.1',
49
83
  '11.0.0.1',
84
+ '100.63.255.255', // just below the CGNAT block
85
+ '100.128.0.0', // just above the CGNAT block
86
+ '198.17.255.255', // just below the benchmarking block
87
+ '198.20.0.0', // just above the benchmarking block
50
88
  '134744072', // decimal-encoded 8.8.8.8 — public
51
89
  '2001:db8::1', // documentation range — public
52
- 'fec0::1', // deprecated site-local, outside fe80::/10 — treated public
90
+ '2002:808:808::', // 6to4 wrapping public 8.8.8.8
91
+ '64:ff9b::808:808', // NAT64 wrapping public 8.8.8.8
53
92
  ]) {
54
93
  assert.equal(isPrivateHost(host), false, `${host} should be public`)
55
94
  }
@@ -138,6 +177,109 @@ describe('safeFetch', () => {
138
177
  )
139
178
  })
140
179
 
180
+ test('refuses a public hostname that resolves to a private address', async () => {
181
+ await withStubbedFetch(
182
+ () => new Response('ok', { status: 200 }),
183
+ async (calls) => {
184
+ // The shape the literal-only check misses: a name that parses as public
185
+ // but whose A record points at the cloud metadata endpoint.
186
+ await assert.rejects(
187
+ safeFetch(
188
+ 'https://169-254-169-254.nip.io/latest/meta-data/',
189
+ {},
190
+ {
191
+ resolveHost: async () => ['169.254.169.254'],
192
+ }
193
+ ),
194
+ /resolves to 169\.254\.169\.254/
195
+ )
196
+ assert.equal(calls.length, 0)
197
+ }
198
+ )
199
+ })
200
+
201
+ test('refuses when any one resolved address is private', async () => {
202
+ await withStubbedFetch(
203
+ () => new Response('ok', { status: 200 }),
204
+ async (calls) => {
205
+ await assert.rejects(
206
+ safeFetch(
207
+ 'https://split-horizon.example.com/',
208
+ {},
209
+ {
210
+ resolveHost: async () => ['93.184.216.34', '10.0.0.5'],
211
+ }
212
+ ),
213
+ /resolves to 10\.0\.0\.5/
214
+ )
215
+ assert.equal(calls.length, 0)
216
+ }
217
+ )
218
+ })
219
+
220
+ test('re-resolves the host of a redirect hop', async () => {
221
+ await withStubbedFetch(
222
+ (url) =>
223
+ url.includes('start.com')
224
+ ? new Response(null, {
225
+ status: 302,
226
+ headers: { location: 'https://rebound.example.com/next' },
227
+ })
228
+ : new Response('ok', { status: 200 }),
229
+ async (calls) => {
230
+ await assert.rejects(
231
+ safeFetch(
232
+ 'https://start.com',
233
+ {},
234
+ {
235
+ resolveHost: async (hostname) =>
236
+ hostname === 'start.com' ? ['93.184.216.34'] : ['127.0.0.1'],
237
+ }
238
+ ),
239
+ /resolves to 127\.0\.0\.1/
240
+ )
241
+ assert.equal(calls.length, 1)
242
+ }
243
+ )
244
+ })
245
+
246
+ test('allows a public hostname that resolves to public addresses', async () => {
247
+ await withStubbedFetch(
248
+ () => new Response('ok', { status: 200 }),
249
+ async (calls) => {
250
+ const res = await safeFetch(
251
+ 'https://example.com/',
252
+ {},
253
+ {
254
+ resolveHost: async () => ['93.184.216.34'],
255
+ }
256
+ )
257
+ assert.equal(res.status, 200)
258
+ assert.equal(calls.length, 1)
259
+ }
260
+ )
261
+ })
262
+
263
+ test('does not resolve an IP literal or an allowlisted host', async () => {
264
+ await withStubbedFetch(
265
+ () => new Response('ok', { status: 200 }),
266
+ async () => {
267
+ let resolverCalls = 0
268
+ const resolveHost = async () => {
269
+ resolverCalls++
270
+ return ['10.0.0.1']
271
+ }
272
+ await safeFetch('https://93.184.216.34/', {}, { resolveHost })
273
+ await safeFetch(
274
+ 'https://internal.example.com/',
275
+ {},
276
+ { resolveHost, allowedHosts: ['internal.example.com'] }
277
+ )
278
+ assert.equal(resolverCalls, 0)
279
+ }
280
+ )
281
+ })
282
+
141
283
  test('follows a redirect to another public host', async () => {
142
284
  await withStubbedFetch(
143
285
  (url) =>
@@ -32,6 +32,134 @@ function parseIPv4Octets(
32
32
  return octets as [number, number, number, number]
33
33
  }
34
34
 
35
+ /**
36
+ * IPv4 blocks that must never be reachable from user-supplied URLs: private,
37
+ * loopback, link-local (cloud metadata), carrier-grade NAT (Alibaba's
38
+ * `100.100.100.200` metadata endpoint), IETF protocol assignments, benchmarking,
39
+ * 6to4 anycast, the documentation TEST-NETs, multicast and reserved space.
40
+ */
41
+ const PRIVATE_IPV4_BLOCKS: ReadonlyArray<readonly [string, number]> = [
42
+ ['0.0.0.0', 8],
43
+ ['10.0.0.0', 8],
44
+ ['100.64.0.0', 10],
45
+ ['127.0.0.0', 8],
46
+ ['169.254.0.0', 16],
47
+ ['172.16.0.0', 12],
48
+ ['192.0.0.0', 24],
49
+ ['192.0.2.0', 24],
50
+ ['192.88.99.0', 24],
51
+ ['192.168.0.0', 16],
52
+ ['198.18.0.0', 15],
53
+ ['198.51.100.0', 24],
54
+ ['203.0.113.0', 24],
55
+ ['224.0.0.0', 4],
56
+ ['240.0.0.0', 4],
57
+ ]
58
+
59
+ const toUint32 = (octets: [number, number, number, number]): number =>
60
+ ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0
61
+
62
+ const PRIVATE_IPV4_RANGES = PRIVATE_IPV4_BLOCKS.map(([base, prefix]) => {
63
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0
64
+ return [(toUint32(parseIPv4Octets(base)!) & mask) >>> 0, mask] as const
65
+ })
66
+
67
+ const isPrivateIPv4 = (octets: [number, number, number, number]): boolean => {
68
+ const addr = toUint32(octets)
69
+ return PRIVATE_IPV4_RANGES.some(
70
+ ([base, mask]) => (addr & mask) >>> 0 === base
71
+ )
72
+ }
73
+
74
+ /**
75
+ * Expands an IPv6 literal into its eight 16-bit groups, handling `::` elision
76
+ * and a trailing dotted-quad. `null` when not a well-formed IPv6 literal.
77
+ */
78
+ function parseIPv6Groups(host: string): number[] | null {
79
+ const zoneless = host.split('%')[0]!
80
+ const halves = zoneless.split('::')
81
+ if (halves.length > 2) return null
82
+
83
+ const parseSide = (side: string): number[] | null => {
84
+ if (side === '') return []
85
+ const parts = side.split(':')
86
+ const groups: number[] = []
87
+ for (let i = 0; i < parts.length; i++) {
88
+ const part = parts[i]!
89
+ if (i === parts.length - 1 && part.includes('.')) {
90
+ const v4 = parseIPv4Octets(part)
91
+ if (!v4) return null
92
+ groups.push((v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3])
93
+ continue
94
+ }
95
+ if (!/^[0-9a-f]{1,4}$/.test(part)) return null
96
+ groups.push(parseInt(part, 16))
97
+ }
98
+ return groups
99
+ }
100
+
101
+ const head = parseSide(halves[0]!)
102
+ if (head === null) return null
103
+
104
+ if (halves.length === 1) {
105
+ return head.length === 8 ? head : null
106
+ }
107
+
108
+ const tail = parseSide(halves[1]!)
109
+ if (tail === null) return null
110
+ if (head.length + tail.length > 7) return null
111
+ return [...head, ...new Array(8 - head.length - tail.length).fill(0), ...tail]
112
+ }
113
+
114
+ const embeddedIPv4 = (
115
+ hi: number,
116
+ lo: number
117
+ ): [number, number, number, number] => [
118
+ (hi >> 8) & 0xff,
119
+ hi & 0xff,
120
+ (lo >> 8) & 0xff,
121
+ lo & 0xff,
122
+ ]
123
+
124
+ function isPrivateIPv6(groups: number[]): boolean {
125
+ const [g0, g1, g2, g3, g4, g5, g6, g7] = groups as [
126
+ number,
127
+ number,
128
+ number,
129
+ number,
130
+ number,
131
+ number,
132
+ number,
133
+ number,
134
+ ]
135
+
136
+ if (groups.every((g) => g === 0)) return true // :: unspecified
137
+ if (groups.slice(0, 7).every((g) => g === 0) && g7 === 1) return true // ::1
138
+
139
+ // IPv4-mapped ::ffff:0:0/96 and IPv4-compatible ::/96
140
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0) {
141
+ if (g5 === 0xffff || g5 === 0) return isPrivateIPv4(embeddedIPv4(g6, g7))
142
+ }
143
+ // NAT64 well-known prefix 64:ff9b::/96
144
+ if (
145
+ g0 === 0x64 &&
146
+ g1 === 0xff9b &&
147
+ g2 === 0 &&
148
+ g3 === 0 &&
149
+ g4 === 0 &&
150
+ g5 === 0
151
+ )
152
+ return isPrivateIPv4(embeddedIPv4(g6, g7))
153
+ // 6to4 2002::/16 carries the IPv4 address in the next 32 bits
154
+ if (g0 === 0x2002) return isPrivateIPv4(embeddedIPv4(g1, g2))
155
+ if (g0 === 0x100 && g1 === 0 && g2 === 0 && g3 === 0) return true // discard-only 100::/64
156
+ if ((g0 & 0xffc0) === 0xfe80) return true // link-local fe80::/10
157
+ if ((g0 & 0xfe00) === 0xfc00) return true // unique-local fc00::/7
158
+ if ((g0 & 0xffc0) === 0xfec0) return true // deprecated site-local fec0::/10
159
+ if ((g0 & 0xff00) === 0xff00) return true // multicast ff00::/8
160
+ return false
161
+ }
162
+
35
163
  /**
36
164
  * Whether a hostname is an obvious internal target. Best-effort literal
37
165
  * matching only: it cannot catch a public hostname that *resolves* to a
@@ -46,31 +174,30 @@ export function isPrivateHost(hostname: string): boolean {
46
174
  return true
47
175
 
48
176
  if (host.includes(':')) {
49
- if (host === '::' || host === '::1') return true
50
- const mappedV4 = host.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)
51
- if (mappedV4) return isPrivateHost(mappedV4[1]!)
52
- const mappedHex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)
53
- if (mappedHex) {
54
- const hi = parseInt(mappedHex[1]!, 16)
55
- const lo = parseInt(mappedHex[2]!, 16)
56
- return isPrivateHost(
57
- `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`
58
- )
59
- }
60
- if (/^fe[89ab]/.test(host)) return true // link-local fe80::/10
61
- if (host.startsWith('fc') || host.startsWith('fd')) return true // unique-local fc00::/7
62
- return false
177
+ const groups = parseIPv6Groups(host)
178
+ return groups ? isPrivateIPv6(groups) : false
63
179
  }
64
180
 
65
181
  const v4 = parseIPv4Octets(host)
66
- if (v4) {
67
- const [a, b] = v4
68
- if (a === 127 || a === 10 || a === 0) return true
69
- if (a === 169 && b === 254) return true // link-local incl. cloud metadata
70
- if (a === 172 && b >= 16 && b <= 31) return true
71
- if (a === 192 && b === 168) return true
72
- }
73
- return false
182
+ return v4 ? isPrivateIPv4(v4) : false
183
+ }
184
+
185
+ /** Resolves a hostname to the IP addresses it points at. */
186
+ export type HostResolver = (hostname: string) => Promise<string[]>
187
+
188
+ let defaultHostResolver: HostResolver | undefined
189
+
190
+ /**
191
+ * Installs the resolver {@link safeFetch} uses when a call passes no
192
+ * `resolveHost` of its own.
193
+ *
194
+ * Core cannot resolve DNS itself — Workers has no DNS API — so without a
195
+ * resolver the guard is literal-only and a public name pointing at
196
+ * `169.254.169.254` passes. Node runtimes install
197
+ * `nodeHostResolver` from `@pikku/core/node-host-resolver` at startup.
198
+ */
199
+ export function setDefaultHostResolver(resolver: HostResolver | undefined) {
200
+ defaultHostResolver = resolver
74
201
  }
75
202
 
76
203
  export interface SafeFetchOptions {
@@ -81,6 +208,50 @@ export interface SafeFetchOptions {
81
208
  allowedHosts?: string[]
82
209
  /** Maximum redirect hops to follow (each re-validated). Defaults to 3. */
83
210
  maxRedirects?: number
211
+ /**
212
+ * Resolves a hostname so a *public* name pointing at a private address is
213
+ * refused. Defaults to whatever {@link setDefaultHostResolver} installed;
214
+ * pass `null` to opt a call out of resolution entirely.
215
+ */
216
+ resolveHost?: HostResolver | null
217
+ }
218
+
219
+ /** Whether a hostname is already an IP literal, which the sync check covers. */
220
+ function isIpLiteral(hostname: string): boolean {
221
+ const host = hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '')
222
+ if (host.includes(':')) return parseIPv6Groups(host.toLowerCase()) !== null
223
+ return parseIPv4Octets(host) !== null
224
+ }
225
+
226
+ /**
227
+ * Rejects a hostname that resolves to an internal address.
228
+ *
229
+ * Resolution happens once per hop and the connection is not pinned to the
230
+ * address checked, so a rebind between this check and the socket connecting is
231
+ * still possible; catching that needs a runtime-level connect hook.
232
+ */
233
+ async function assertResolvedHostAllowed(
234
+ hostname: string,
235
+ options: SafeFetchOptions
236
+ ): Promise<void> {
237
+ if (options.allowedHosts) return
238
+ const resolver =
239
+ options.resolveHost === null
240
+ ? undefined
241
+ : (options.resolveHost ?? defaultHostResolver)
242
+ if (!resolver || isIpLiteral(hostname)) return
243
+
244
+ const addresses = await resolver(hostname)
245
+ if (addresses.length === 0) {
246
+ throw new Error(`Refusing to fetch: '${hostname}' resolved to no addresses`)
247
+ }
248
+ for (const address of addresses) {
249
+ if (isPrivateHost(address)) {
250
+ throw new Error(
251
+ `Refusing to fetch from a private/internal host: '${hostname}' resolves to ${address}`
252
+ )
253
+ }
254
+ }
84
255
  }
85
256
 
86
257
  export function assertFetchableUrl(
@@ -142,7 +313,9 @@ export async function safeFetch(
142
313
  options: SafeFetchOptions = {}
143
314
  ): Promise<Response> {
144
315
  const maxRedirects = options.maxRedirects ?? 3
145
- let currentUrl = assertFetchableUrl(url, options).toString()
316
+ const initial = assertFetchableUrl(url, options)
317
+ await assertResolvedHostAllowed(initial.hostname, options)
318
+ let currentUrl = initial.toString()
146
319
  let currentInit = init
147
320
 
148
321
  for (let hop = 0; ; hop++) {
@@ -157,10 +330,12 @@ export async function safeFetch(
157
330
  if (!location || hop >= maxRedirects) {
158
331
  return response
159
332
  }
160
- const nextUrl = assertFetchableUrl(
333
+ const next = assertFetchableUrl(
161
334
  new URL(location, currentUrl).toString(),
162
335
  options
163
- ).toString()
336
+ )
337
+ await assertResolvedHostAllowed(next.hostname, options)
338
+ const nextUrl = next.toString()
164
339
  await response.body?.cancel()
165
340
  let nextInit = redirectInit(response.status, currentInit)
166
341
  if (new URL(nextUrl).origin !== new URL(currentUrl).origin) {
@@ -119,12 +119,7 @@ function actorInstructions(persona: ResolvedPersona, task: string): string {
119
119
  : '',
120
120
  `Your goal in this conversation: ${task}.`,
121
121
  `Send one message at a time. Set "done" to true only once your goal is clearly accomplished, or clearly impossible.`,
122
- // Every call the actor makes wants a schema'd object back, and a gateway
123
- // that cannot take a JSON *schema* degrades to OpenAI's `json_object`
124
- // mode — which refuses the request outright unless the word "json" appears
125
- // somewhere in the prompt. Saying it once here covers all three call sites
126
- // (turn, approvals, verdict), since each builds on these instructions, and
127
- // it costs nothing on providers that never needed telling.
122
+ // knowledge: decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md
128
123
  `Reply with json matching the schema you are given, and nothing else.`,
129
124
  ]
130
125
  .filter(Boolean)
@@ -0,0 +1,120 @@
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
+ import type { AIAgentInput, AIStreamChannel } from './ai-agent.types.js'
6
+ import type { StreamAIAgentOptions } from './ai-agent-prepare.js'
7
+ import { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js'
8
+ import {
9
+ streamAIAgent,
10
+ resumeAIAgent,
11
+ interruptAIAgent,
12
+ } from './ai-agent-stream.js'
13
+ import { wrapChannelWithAGUI } from './ai-agent-agui.js'
14
+
15
+ export type AgentRPCOptions = {
16
+ sessionService?: SessionService<CoreUserSession>
17
+ }
18
+
19
+ /**
20
+ * `wire.rpc.agent`, implemented.
21
+ *
22
+ * Lives here rather than in `rpc-runner` so the agent surface is one file next
23
+ * to the runner and stream code it delegates to, instead of a wing of the RPC
24
+ * primitive.
25
+ */
26
+ export const createAgentRPC = (
27
+ wire: PikkuRawWire,
28
+ options: AgentRPCOptions
29
+ ): PikkuRPC['agent'] => {
30
+ const params = () => ({
31
+ sessionService: options.sessionService,
32
+ getCredential: wire.getCredential?.bind(wire),
33
+ })
34
+
35
+ const streamingChannel = (): AIStreamChannel => {
36
+ const channel = wire.channel as unknown as AIStreamChannel | undefined
37
+ if (!channel) throw new Error('No channel available for streaming')
38
+ return channel
39
+ }
40
+
41
+ /** `run` and `approve` return the same shape; only the call differs. */
42
+ const asRunResult = (result: Awaited<ReturnType<typeof runAIAgent>>) => ({
43
+ runId: result.runId,
44
+ result: result.object ?? result.text,
45
+ usage: result.usage,
46
+ ...(result.status === 'suspended' && {
47
+ status: 'suspended' as const,
48
+ pendingApprovals: result.pendingApprovals,
49
+ }),
50
+ })
51
+
52
+ return {
53
+ run: async (agentName: string, input: AIAgentInput) =>
54
+ asRunResult(await runAIAgent(agentName, input, params())),
55
+
56
+ stream: async (
57
+ agentName: string,
58
+ input: {
59
+ message: string
60
+ threadId: string
61
+ resourceId: string
62
+ model?: string
63
+ temperature?: number
64
+ },
65
+ streamOptions?: StreamAIAgentOptions
66
+ ) => {
67
+ let currentRunId: string | undefined
68
+ await streamAIAgent(
69
+ agentName,
70
+ input,
71
+ wrapChannelWithAGUI(streamingChannel(), {
72
+ threadId: input.threadId,
73
+ getRunId: () => currentRunId,
74
+ }),
75
+ params(),
76
+ undefined,
77
+ {
78
+ ...streamOptions,
79
+ onRunCreated: (runId) => {
80
+ currentRunId = runId
81
+ streamOptions?.onRunCreated?.(runId)
82
+ },
83
+ }
84
+ )
85
+ },
86
+
87
+ resume: async (
88
+ runId: string,
89
+ input: { toolCallId: string; approved: boolean },
90
+ streamOptions?: StreamAIAgentOptions
91
+ ) => {
92
+ await resumeAIAgent(
93
+ { runId, ...input },
94
+ wrapChannelWithAGUI(streamingChannel(), { runId }),
95
+ params(),
96
+ streamOptions
97
+ )
98
+ },
99
+
100
+ interrupt: async (runId: string, reason?: 'speech' | 'user' | 'timeout') =>
101
+ interruptAIAgent(
102
+ { runId, ...(reason ? { reason } : {}) },
103
+ { sessionService: options.sessionService }
104
+ ),
105
+
106
+ approve: async (
107
+ runId: string,
108
+ approvals: { toolCallId: string; approved: boolean }[],
109
+ expectedAgentName?: string
110
+ ) =>
111
+ asRunResult(
112
+ await resumeAIAgentSync(
113
+ runId,
114
+ approvals,
115
+ { sessionService: options.sessionService },
116
+ expectedAgentName
117
+ )
118
+ ),
119
+ }
120
+ }
@@ -372,11 +372,7 @@ export function wrapChannelWithAGUI(
372
372
  break
373
373
  }
374
374
 
375
- // AG-UI has no event for speech, so it travels as CUSTOM like the other
376
- // pikku-specific ones. Dropping it instead — which is what this did —
377
- // means a voice agent reached over HTTP is inaudible: `voiceOutput`
378
- // synthesizes every sentence, the provider bills for it, and nothing
379
- // gets past the mapper.
375
+ // knowledge: decisions/internals/agent-speech-travels-as-a-custom-agui-event.md
380
376
  case 'audio-delta': {
381
377
  send({
382
378
  type: 'CUSTOM',
@@ -574,7 +574,7 @@ describe('non-streaming agent interruption', () => {
574
574
  updateRun: async (_runId: string, patch: unknown) => {
575
575
  updates.push(patch)
576
576
  },
577
- resolveApproval: async () => {},
577
+ resolveApproval: async () => true,
578
578
  },
579
579
  } as any)
580
580
 
@@ -648,6 +648,7 @@ describe('streaming resume interruption', () => {
648
648
  // for approvals that have already been given.
649
649
  resolveApproval: async () => {
650
650
  run.pendingApprovals = []
651
+ return true
651
652
  },
652
653
  },
653
654
  } as any)
@@ -19,7 +19,14 @@ export function resolveMemoryServices(
19
19
  } {
20
20
  const memoryConfig = agent.memory
21
21
  const storage = memoryConfig?.storage
22
- ? (singletonServices as any)[memoryConfig.storage]
22
+ ? // The service is named by string in agent config, so the lookup cannot be
23
+ // checked — only the shape it is required to have.
24
+ (
25
+ singletonServices as unknown as Record<
26
+ string,
27
+ AIStorageService | undefined
28
+ >
29
+ )[memoryConfig.storage]
23
30
  : singletonServices.aiStorage
24
31
  return { storage }
25
32
  }