@pikku/core 0.12.84 → 0.12.86

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 (564) hide show
  1. package/CHANGELOG.md +424 -0
  2. package/dist/bootstrap-compat/root.d.ts +20 -0
  3. package/dist/classification/column-form.d.ts +32 -0
  4. package/dist/classification/column-form.js +42 -0
  5. package/dist/classification/data-classification.d.ts +64 -0
  6. package/dist/classification/index.d.ts +13 -0
  7. package/dist/classification/index.js +2 -0
  8. package/dist/classification/secret-value.d.ts +56 -0
  9. package/dist/classification/secret-value.js +46 -0
  10. package/dist/crypto-utils.d.ts +1 -1
  11. package/dist/ecosystem/agent-scorer.d.ts +1 -1
  12. package/dist/ecosystem/agent.d.ts +4 -1
  13. package/dist/ecosystem/channel/local.d.ts +2 -0
  14. package/dist/ecosystem/channel/local.js +2 -0
  15. package/dist/ecosystem/channel/serverless.d.ts +9 -0
  16. package/dist/ecosystem/channel/serverless.js +1 -0
  17. package/dist/ecosystem/channel.d.ts +3 -0
  18. package/dist/ecosystem/channel.js +1 -0
  19. package/dist/ecosystem/cli.d.ts +1 -1
  20. package/dist/ecosystem/cli.js +1 -1
  21. package/dist/ecosystem/credential.d.ts +1 -0
  22. package/dist/ecosystem/crypto-utils.d.ts +8 -0
  23. package/dist/ecosystem/crypto-utils.js +1 -0
  24. package/dist/ecosystem/dev.d.ts +1 -0
  25. package/dist/ecosystem/dev.js +1 -0
  26. package/{src/ecosystem/testing.ts → dist/ecosystem/errors.d.ts} +3 -3
  27. package/dist/ecosystem/errors.js +2 -0
  28. package/dist/ecosystem/function.d.ts +1 -1
  29. package/dist/ecosystem/function.js +1 -1
  30. package/dist/ecosystem/http.d.ts +3 -1
  31. package/dist/ecosystem/http.js +3 -1
  32. package/dist/ecosystem/mcp.d.ts +1 -1
  33. package/dist/ecosystem/mcp.js +1 -1
  34. package/dist/ecosystem/middleware.d.ts +2 -1
  35. package/dist/ecosystem/middleware.js +2 -1
  36. package/dist/ecosystem/persona.d.ts +4 -2
  37. package/dist/ecosystem/persona.js +2 -1
  38. package/dist/ecosystem/queue.d.ts +4 -2
  39. package/dist/ecosystem/queue.js +3 -1
  40. package/dist/ecosystem/role.d.ts +1 -0
  41. package/dist/ecosystem/rpc.d.ts +2 -0
  42. package/dist/ecosystem/rpc.js +1 -0
  43. package/dist/ecosystem/scenario.d.ts +8 -2
  44. package/dist/ecosystem/scenario.js +5 -1
  45. package/dist/ecosystem/scheduler.d.ts +2 -1
  46. package/dist/ecosystem/scheduler.js +2 -1
  47. package/dist/ecosystem/schema.d.ts +2 -1
  48. package/dist/ecosystem/schema.js +1 -1
  49. package/dist/ecosystem/scope.d.ts +2 -0
  50. package/dist/ecosystem/scope.js +1 -0
  51. package/dist/ecosystem/secret.d.ts +2 -0
  52. package/dist/ecosystem/secret.js +1 -0
  53. package/dist/ecosystem/services/local-content.d.ts +2 -1
  54. package/dist/ecosystem/services/local-content.js +1 -1
  55. package/dist/ecosystem/services/local-meta.d.ts +1 -0
  56. package/dist/ecosystem/services/local-meta.js +1 -0
  57. package/dist/ecosystem/services.d.ts +11 -3
  58. package/dist/ecosystem/services.js +7 -1
  59. package/dist/ecosystem/trigger.d.ts +1 -0
  60. package/dist/ecosystem/trigger.js +1 -1
  61. package/dist/ecosystem/types.d.ts +6 -3
  62. package/dist/ecosystem/types.js +3 -2
  63. package/dist/ecosystem/variable.d.ts +1 -0
  64. package/dist/ecosystem/virtual-user.d.ts +3 -0
  65. package/dist/ecosystem/virtual-user.js +3 -0
  66. package/dist/ecosystem/workflow.d.ts +2 -0
  67. package/dist/ecosystem/workflow.js +2 -0
  68. package/dist/errors/index.d.ts +8 -0
  69. package/dist/errors/index.js +1 -0
  70. package/dist/errors/serialized-error.d.ts +7 -0
  71. package/dist/function/function-meta.types.d.ts +139 -0
  72. package/dist/function/function-runner.d.ts +3 -2
  73. package/dist/function/function-runner.js +31 -11
  74. package/dist/function/functions.types.d.ts +10 -2
  75. package/dist/function/index.d.ts +8 -2
  76. package/dist/function/index.js +4 -1
  77. package/dist/function/pikku-request.d.ts +8 -0
  78. package/dist/function/pikku-request.js +15 -0
  79. package/dist/middleware/auth-apikey.d.ts +2 -10
  80. package/dist/middleware/auth-apikey.js +1 -1
  81. package/dist/middleware/auth-bearer.d.ts +2 -10
  82. package/dist/middleware/auth-bearer.js +1 -1
  83. package/dist/middleware/auth-cookie.d.ts +2 -10
  84. package/dist/middleware/auth-cookie.js +1 -1
  85. package/dist/middleware/cors.d.ts +5 -11
  86. package/dist/middleware/cors.js +13 -6
  87. package/dist/middleware/index.d.ts +3 -0
  88. package/dist/middleware/index.js +1 -0
  89. package/dist/middleware/middleware-factories.d.ts +9 -0
  90. package/dist/middleware/middleware-factories.js +19 -0
  91. package/dist/middleware/middleware.types.d.ts +27 -0
  92. package/dist/middleware/middleware.types.js +1 -0
  93. package/dist/middleware/remote-auth.d.ts +2 -2
  94. package/dist/middleware/remote-auth.js +1 -1
  95. package/dist/middleware/telemetry.d.ts +4 -20
  96. package/dist/middleware/telemetry.js +1 -1
  97. package/dist/middleware-runner.d.ts +2 -1
  98. package/dist/pikku-state.d.ts +2 -1
  99. package/dist/services/audit-service.d.ts +1 -1
  100. package/dist/services/credential-wire-service.js +1 -1
  101. package/dist/services/email-service.d.ts +1 -1
  102. package/dist/services/file-scenario-run-store.d.ts +58 -0
  103. package/dist/services/file-scenario-run-store.js +214 -0
  104. package/dist/services/in-memory-workflow-service.d.ts +1 -1
  105. package/dist/services/index.d.ts +5 -1
  106. package/dist/services/index.js +3 -0
  107. package/dist/services/local-secrets.d.ts +1 -1
  108. package/dist/services/local-secrets.js +1 -1
  109. package/dist/services/logger.d.ts +7 -7
  110. package/dist/services/meta-service.d.ts +2 -1
  111. package/dist/services/scoped-secret-service.d.ts +1 -1
  112. package/dist/services/secret-service.d.ts +1 -1
  113. package/dist/services/typed-secret-service.d.ts +16 -2
  114. package/dist/services/typed-secret-service.js +5 -0
  115. package/dist/services/webhook-service.d.ts +1 -1
  116. package/dist/services/workflow-service.d.ts +2 -1
  117. package/dist/testing/index.d.ts +1 -0
  118. package/dist/testing/index.js +1 -0
  119. package/dist/types/core.types.d.ts +30 -180
  120. package/dist/types/core.types.js +1 -19
  121. package/dist/types/index.d.ts +2 -0
  122. package/dist/types/index.js +1 -0
  123. package/dist/types/state.types.d.ts +3 -1
  124. package/dist/utils.d.ts +13 -0
  125. package/dist/utils.js +2 -0
  126. package/dist/wirings/addon/addon-runner.d.ts +17 -0
  127. package/dist/wirings/addon/addon-runner.js +150 -0
  128. package/dist/wirings/addon/index.d.ts +6 -0
  129. package/dist/wirings/addon/index.js +3 -0
  130. package/dist/wirings/addon/remote-addon-auth.d.ts +16 -0
  131. package/dist/wirings/addon/remote-addon-auth.js +39 -0
  132. package/dist/wirings/addon/wire-addon.d.ts +80 -0
  133. package/dist/wirings/addon/wire-addon.js +120 -0
  134. package/dist/wirings/addon/wire-remote-addon.d.ts +22 -0
  135. package/dist/wirings/addon/wire-remote-addon.js +11 -0
  136. package/dist/wirings/agent/agent-prepare.d.ts +3 -3
  137. package/dist/wirings/agent/agent-prepare.js +1 -1
  138. package/dist/wirings/agent/agent-runner.js +2 -3
  139. package/dist/wirings/agent/agent-stream.js +2 -2
  140. package/dist/wirings/agent/agent-utils.d.ts +13 -0
  141. package/dist/wirings/agent/agent-utils.js +16 -0
  142. package/dist/wirings/agent/agent.types.d.ts +3 -2
  143. package/dist/wirings/agent/index.d.ts +1 -1
  144. package/dist/wirings/agent/voice-input.js +1 -1
  145. package/dist/wirings/agent/voice-output.js +1 -1
  146. package/dist/wirings/agent-scorer/agent-scorer-judge.d.ts +13 -0
  147. package/dist/wirings/agent-scorer/agent-scorer-judge.js +52 -0
  148. package/dist/wirings/agent-scorer/agent-scorer.d.ts +7 -1
  149. package/dist/wirings/agent-scorer/agent-scorer.js +1 -0
  150. package/dist/wirings/agent-scorer/agent-scorer.types.d.ts +20 -0
  151. package/dist/wirings/agent-scorer/index.d.ts +1 -1
  152. package/dist/wirings/channel/channel-common.d.ts +2 -1
  153. package/dist/wirings/channel/channel-middleware-runner.d.ts +2 -1
  154. package/dist/wirings/channel/channel.types.d.ts +1 -1
  155. package/dist/wirings/channel/serverless/index.d.ts +1 -0
  156. package/dist/wirings/cli/channel/index.d.ts +1 -1
  157. package/dist/wirings/cli/cli-runner.d.ts +2 -1
  158. package/dist/wirings/cli/cli-runner.js +4 -1
  159. package/dist/wirings/cli/cli.types.d.ts +21 -2
  160. package/dist/wirings/cli/command-parser.js +157 -23
  161. package/dist/wirings/gateway/gateway.types.d.ts +2 -1
  162. package/dist/wirings/http/http-runner.d.ts +1 -1
  163. package/dist/wirings/http/http-runner.js +1 -1
  164. package/dist/wirings/http/http.types.d.ts +2 -2
  165. package/dist/wirings/http/index.d.ts +1 -0
  166. package/dist/wirings/http/index.js +1 -0
  167. package/dist/wirings/http/pikku-fetch-http-request.js +1 -1
  168. package/dist/wirings/http/pikku-fetch-http-response.js +2 -2
  169. package/dist/wirings/mcp/mcp.types.d.ts +1 -1
  170. package/dist/wirings/persona/index.d.ts +1 -1
  171. package/dist/wirings/persona/index.js +1 -1
  172. package/dist/wirings/persona/persona.types.d.ts +14 -0
  173. package/dist/wirings/queue/queue.types.d.ts +1 -1
  174. package/dist/wirings/queue/signed-queue-service.d.ts +1 -1
  175. package/dist/wirings/rpc/index.d.ts +0 -6
  176. package/dist/wirings/rpc/index.js +0 -3
  177. package/dist/wirings/rpc/remote-addon-auth.d.ts +1 -1
  178. package/dist/wirings/rpc/remote-addon-auth.js +1 -1
  179. package/dist/wirings/rpc/rpc-runner.js +2 -2
  180. package/dist/wirings/rpc/wire-addon.d.ts +1 -1
  181. package/dist/wirings/scheduler/scheduler.types.d.ts +2 -1
  182. package/dist/wirings/scope/index.d.ts +1 -0
  183. package/dist/wirings/scope/index.js +1 -0
  184. package/dist/wirings/secret/secret.types.d.ts +10 -0
  185. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  186. package/dist/wirings/variable/validate-variable-definitions.js +2 -0
  187. package/dist/wirings/variable/variable.types.d.ts +9 -0
  188. package/dist/wirings/virtual-user/index.d.ts +1 -1
  189. package/dist/wirings/virtual-user/index.js +1 -1
  190. package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +1 -1
  191. package/dist/wirings/virtual-user/virtual-user-derive.d.ts +1 -1
  192. package/dist/wirings/workflow/graph/graph-node.d.ts +9 -2
  193. package/dist/wirings/workflow/graph/graph-node.js +2 -0
  194. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -0
  195. package/dist/wirings/workflow/graph/graph-runner.js +283 -31
  196. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +16 -1
  197. package/dist/wirings/workflow/index.d.ts +2 -0
  198. package/dist/wirings/workflow/index.js +1 -0
  199. package/dist/wirings/workflow/pikku-scenario-service.d.ts +14 -4
  200. package/dist/wirings/workflow/pikku-scenario-service.js +44 -16
  201. package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -1
  202. package/dist/wirings/workflow/pikku-workflow-service.js +3 -4
  203. package/dist/wirings/workflow/run-timeline.d.ts +1 -1
  204. package/dist/wirings/workflow/scenario-run.types.d.ts +171 -0
  205. package/dist/wirings/workflow/scenario-run.types.js +1 -0
  206. package/dist/wirings/workflow/scenario-step-guards.d.ts +1 -2
  207. package/dist/wirings/workflow/scenario-step-guards.js +0 -8
  208. package/dist/wirings/workflow/scenario-step.types.d.ts +27 -12
  209. package/dist/wirings/workflow/workflow-approval.js +2 -1
  210. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +2 -1
  211. package/dist/wirings/workflow/workflow.types.d.ts +2 -1
  212. package/knowledge/decisions/index.md +2 -0
  213. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +1 -1
  214. package/knowledge/decisions/internals/a-secret-that-fails-to-decrypt-fails-the-whole-read.md +1 -1
  215. package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-and-not-a-queued-job.md +2 -2
  216. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +6 -6
  217. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +1 -1
  218. package/knowledge/decisions/internals/addon-singleton-services-are-cached-per-namespace-not-per-package.md +1 -1
  219. package/knowledge/decisions/internals/addon-workflow-names-are-prefixed-with-the-consumer-namespace.md +1 -1
  220. package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +1 -1
  221. package/knowledge/decisions/internals/channel-message-handlers-accept-three-config-shapes.md +1 -1
  222. package/knowledge/decisions/internals/channel-middleware-caches-only-statically-resolved-middleware.md +1 -1
  223. package/knowledge/decisions/internals/channel-user-id-is-persisted-after-onconnect-middleware-runs.md +1 -1
  224. package/knowledge/decisions/internals/cli-option-names-are-camelcase-in-state-and-kebab-on-the-command-line.md +1 -1
  225. package/knowledge/decisions/internals/cli-parse-errors-are-routed-by-message-prefix.md +1 -1
  226. package/knowledge/decisions/internals/cli-unknown-long-options-warn-instead-of-failing.md +1 -1
  227. package/knowledge/decisions/internals/core-column-form-is-an-axis-of-its-own.md +4 -4
  228. package/knowledge/decisions/internals/core-data-classification-brand-is-an-optional-property.md +1 -1
  229. package/knowledge/decisions/internals/core-function-runner-restores-the-wire-fields-it-overwrites.md +3 -3
  230. package/knowledge/decisions/internals/core-hot-reload-merges-generated-meta-never-replaces-it.md +2 -2
  231. package/knowledge/decisions/internals/core-hot-reload-owns-its-module-registry.md +2 -2
  232. package/knowledge/decisions/internals/core-middleware-order-is-scope-then-priority.md +1 -1
  233. package/knowledge/decisions/internals/core-schema-defaults-apply-on-every-transport.md +1 -1
  234. package/knowledge/decisions/internals/core-scopes-are-an-and-gate-separate-from-permissions.md +3 -3
  235. package/knowledge/decisions/internals/gateway-adapters-resolve-lazily-and-are-promise-cached.md +1 -1
  236. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +4 -4
  237. package/knowledge/decisions/internals/gateway-wiring-is-a-meta-wiring-over-http-and-channels.md +1 -1
  238. package/knowledge/decisions/internals/http-router-matches-normalized-paths-but-returns-registered-ones.md +1 -1
  239. package/knowledge/decisions/internals/in-a-scenario-a-4xx-is-data-not-an-exception.md +1 -1
  240. package/knowledge/decisions/internals/in-memory-workflow-history-aliases-the-live-step-object.md +1 -1
  241. package/knowledge/decisions/internals/index.md +4 -2
  242. package/knowledge/decisions/internals/istanbul-statement-counts-attach-to-the-start-line-only.md +1 -1
  243. package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +1 -1
  244. package/knowledge/decisions/internals/queue-jobs-always-carry-an-explicit-attempts-count.md +1 -1
  245. package/knowledge/decisions/internals/remote-addons-dispatch-over-http-instead-of-local-meta.md +1 -1
  246. package/knowledge/decisions/internals/rpc-names-resolve-through-package-scope-before-root.md +1 -1
  247. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +2 -2
  248. package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +5 -5
  249. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +1 -1
  250. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +1 -1
  251. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +43 -19
  252. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +1 -1
  253. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +1 -1
  254. package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +3 -3
  255. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +1 -1
  256. package/knowledge/decisions/internals/webhook-delivery-history-records-every-attempt-best-effort.md +1 -1
  257. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +1 -1
  258. package/knowledge/decisions/internals/workflow-approval-expiry-is-decided-from-a-recorded-deadline.md +1 -1
  259. package/knowledge/decisions/internals/workflow-core-never-imports-a-browser-driver.md +1 -1
  260. package/knowledge/decisions/internals/workflow-inline-runs-report-their-run-id-before-they-can-fail.md +1 -1
  261. package/knowledge/decisions/internals/workflow-invocation-id-is-the-dedupe-key-not-step-id.md +1 -1
  262. package/knowledge/decisions/internals/workflow-run-capabilities-are-extensions-not-subclasses.md +1 -1
  263. package/knowledge/decisions/internals/workflow-run-timeline-is-a-pure-fold-over-durable-history.md +1 -1
  264. package/knowledge/decisions/internals/workflow-scenario-assertions-never-retry-and-record-one-step.md +9 -5
  265. package/knowledge/decisions/internals/workflow-scenario-hooks-are-a-scenario-only-affordance.md +1 -1
  266. package/knowledge/decisions/internals/workflow-step-rpc-name-is-provenance-only.md +2 -2
  267. package/knowledge/decisions/internals/workflow-suspend-and-approval-reasons-are-durable-step-identities.md +1 -1
  268. package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +1 -1
  269. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +1 -1
  270. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +2 -3
  271. package/knowledge/decisions/security/actor-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md +1 -1
  272. package/knowledge/decisions/security/addon-auth-and-tags-only-tighten.md +1 -1
  273. package/knowledge/decisions/security/agent-gate-requires-a-session-only-when-auth-is-true.md +1 -1
  274. package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +3 -3
  275. package/knowledge/decisions/security/console-addon-privileged-functions-gate-themselves.md +4 -3
  276. package/knowledge/decisions/security/core-safe-fetch-blocks-ssrf-by-host-literal-not-dns.md +1 -1
  277. package/knowledge/decisions/security/gateway-middleware-sessions-must-be-bridged-onto-the-wire.md +2 -2
  278. package/knowledge/decisions/security/global-permissions-and-function-permissions-are-independent-gates.md +1 -1
  279. package/knowledge/decisions/security/http-request-bodies-are-bounded-before-they-are-buffered.md +2 -2
  280. package/knowledge/decisions/security/index.md +2 -0
  281. package/knowledge/decisions/security/mcp-internal-error-details-are-double-gated-on-production.md +1 -1
  282. package/knowledge/decisions/security/queue-jobs-carry-the-producers-pikku-user-id.md +3 -3
  283. package/knowledge/decisions/security/remote-addon-tokens-are-client-credentials-not-mesh-trust.md +1 -1
  284. package/knowledge/decisions/security/scaffold-features-are-authenticated-unless-opted-out.md +2 -2
  285. package/knowledge/decisions/security/self-authentication-is-declared-not-detected.md +1 -1
  286. package/knowledge/decisions/security/signed-content-urls-bind-the-request-path.md +2 -2
  287. package/knowledge/index.md +3 -1
  288. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +1 -1
  289. package/knowledge/questions/index.md +2 -0
  290. package/package.json +14 -13
  291. package/run-tests.sh +0 -0
  292. package/scripts/generate-api-report.d.mts +1 -1
  293. package/scripts/generate-api-report.mjs +67 -60
  294. package/scripts/generate-api-report.mts +11 -45
  295. package/src/api-report.test.ts +70 -1
  296. package/src/app-alias-surface.test.ts +179 -0
  297. package/src/app-leaf-surface.test.ts +217 -0
  298. package/src/bootstrap-compat/root.ts +20 -0
  299. package/src/{column-form.test.ts → classification/column-form.test.ts} +2 -2
  300. package/src/{data-classification.ts → classification/data-classification.ts} +1 -5
  301. package/src/classification/index.ts +38 -0
  302. package/src/{secret-value.test.ts → classification/secret-value.test.ts} +2 -2
  303. package/src/{secret-value.ts → classification/secret-value.ts} +1 -6
  304. package/src/crypto-utils.ts +1 -1
  305. package/src/dev/reload-meta.test.ts +1 -1
  306. package/src/errors/index.ts +9 -0
  307. package/src/errors/serialized-error.ts +9 -0
  308. package/src/factory-functions.test.ts +4 -2
  309. package/src/function/function-meta.types.ts +151 -0
  310. package/src/function/function-runner.test.ts +4 -2
  311. package/src/function/function-runner.ts +47 -14
  312. package/src/function/functions.types.ts +11 -7
  313. package/src/function/index.ts +24 -5
  314. package/src/function/list.types.ts +1 -3
  315. package/src/middleware/auth-apikey.ts +4 -2
  316. package/src/middleware/auth-bearer.test.ts +1 -1
  317. package/src/middleware/auth-bearer.ts +4 -2
  318. package/src/middleware/auth-cookie.ts +4 -1
  319. package/src/middleware/cors.test.ts +126 -5
  320. package/src/middleware/cors.ts +19 -8
  321. package/src/middleware/index.ts +16 -0
  322. package/src/middleware/middleware-factories.ts +62 -0
  323. package/src/middleware/middleware.types.ts +61 -0
  324. package/src/middleware/remote-auth.test.ts +1 -1
  325. package/src/middleware/remote-auth.ts +1 -1
  326. package/src/middleware/telemetry.ts +4 -2
  327. package/src/middleware-runner.test.ts +2 -3
  328. package/src/middleware-runner.ts +2 -2
  329. package/src/no-root-barrel.test.ts +114 -0
  330. package/src/permissions.ts +2 -4
  331. package/src/pikku-state.ts +3 -1
  332. package/src/public-surface.json +56 -241
  333. package/src/public-surface.json.README +3 -3
  334. package/src/public-surface.test.ts +5 -5
  335. package/src/remote.test.ts +1 -1
  336. package/src/removed-legacy-exports.test.ts +5 -1
  337. package/src/services/audit-service.ts +1 -1
  338. package/src/services/credential-wire-service.ts +1 -1
  339. package/src/services/email-service.ts +2 -4
  340. package/src/services/file-scenario-run-store.test.ts +349 -0
  341. package/src/services/file-scenario-run-store.ts +282 -0
  342. package/src/services/in-memory-workflow-service.ts +1 -1
  343. package/src/services/index.ts +13 -1
  344. package/src/services/local-content-request-handler.ts +1 -2
  345. package/src/services/local-secrets.ts +1 -1
  346. package/src/services/logger.ts +7 -7
  347. package/src/services/meta-service.ts +2 -2
  348. package/src/services/queue-webhook-service.test.ts +1 -1
  349. package/src/services/scoped-secret-service.ts +1 -1
  350. package/src/services/secret-service.ts +1 -1
  351. package/src/services/typed-secret-service.test.ts +52 -0
  352. package/src/services/typed-secret-service.ts +26 -2
  353. package/src/services/webhook-service.ts +1 -1
  354. package/src/services/workflow-service.ts +2 -1
  355. package/src/template-alias-surface.test.ts +240 -0
  356. package/src/testing/index.ts +1 -0
  357. package/src/types/core.types.ts +33 -285
  358. package/src/types/index.ts +2 -0
  359. package/src/types/state.types.ts +6 -5
  360. package/src/types/typed-credentials.test.ts +72 -0
  361. package/src/utils.ts +34 -0
  362. package/src/wirings/{rpc → addon}/addon-scopes.test.ts +1 -1
  363. package/src/wirings/addon/index.ts +12 -0
  364. package/src/wirings/{rpc → addon}/remote-addon-auth.ts +4 -1
  365. package/src/wirings/{rpc → addon}/wire-addon.ts +37 -2
  366. package/src/wirings/agent/agent-prepare.ts +8 -5
  367. package/src/wirings/agent/agent-runner.ts +2 -3
  368. package/src/wirings/agent/agent-stream.ts +2 -3
  369. package/src/wirings/agent/agent-utils.ts +19 -0
  370. package/src/wirings/agent/agent.types.ts +3 -3
  371. package/src/wirings/agent/index.ts +1 -0
  372. package/src/wirings/agent/voice-input.ts +1 -1
  373. package/src/wirings/agent/voice-output.test.ts +2 -4
  374. package/src/wirings/agent/voice-output.ts +1 -1
  375. package/src/wirings/agent-scorer/agent-scorer-judge.test.ts +145 -0
  376. package/src/wirings/agent-scorer/agent-scorer-judge.ts +60 -0
  377. package/src/wirings/agent-scorer/agent-scorer.ts +8 -0
  378. package/src/wirings/agent-scorer/agent-scorer.types.ts +21 -0
  379. package/src/wirings/agent-scorer/index.ts +1 -0
  380. package/src/wirings/channel/channel-common.ts +4 -2
  381. package/src/wirings/channel/channel-handler.ts +2 -5
  382. package/src/wirings/channel/channel-middleware-runner.ts +1 -1
  383. package/src/wirings/channel/channel-rpc.types.ts +1 -5
  384. package/src/wirings/channel/channel.types.ts +1 -2
  385. package/src/wirings/channel/local/local-channel-runner.test.ts +1 -2
  386. package/src/wirings/channel/serverless/index.ts +1 -0
  387. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +2 -2
  388. package/src/wirings/cli/channel/index.ts +1 -3
  389. package/src/wirings/cli/cli-runner.test.ts +1 -1
  390. package/src/wirings/cli/cli-runner.ts +11 -7
  391. package/src/wirings/cli/cli.types.ts +24 -3
  392. package/src/wirings/cli/command-parser.test.ts +388 -1
  393. package/src/wirings/cli/command-parser.ts +189 -29
  394. package/src/wirings/gateway/gateway-runner.test.ts +1 -2
  395. package/src/wirings/gateway/gateway-runner.ts +3 -6
  396. package/src/wirings/gateway/gateway.types.ts +5 -4
  397. package/src/wirings/http/http-routes.test.ts +1 -2
  398. package/src/wirings/http/http-runner-addon-ref.test.ts +238 -0
  399. package/src/wirings/http/http-runner.test.ts +3 -8
  400. package/src/wirings/http/http-runner.ts +9 -3
  401. package/src/wirings/http/http.types.ts +3 -12
  402. package/src/wirings/http/index.ts +1 -0
  403. package/src/wirings/http/pikku-fetch-http-request.ts +1 -1
  404. package/src/wirings/http/pikku-fetch-http-response.ts +3 -2
  405. package/src/wirings/mcp/mcp-runner.test.ts +1 -1
  406. package/src/wirings/mcp/mcp.types.ts +1 -2
  407. package/src/wirings/persona/index.ts +1 -4
  408. package/src/wirings/persona/persona.types.ts +14 -0
  409. package/src/wirings/queue/queue-identity.test.ts +1 -1
  410. package/src/wirings/queue/queue.types.ts +2 -6
  411. package/src/wirings/queue/signed-queue-service.ts +1 -1
  412. package/src/wirings/rpc/index.ts +0 -12
  413. package/src/wirings/rpc/rpc-runner.test.ts +4 -4
  414. package/src/wirings/rpc/rpc-runner.ts +3 -3
  415. package/src/wirings/scheduler/scheduler.types.ts +2 -5
  416. package/src/wirings/scope/index.ts +1 -0
  417. package/src/wirings/secret/secret.types.ts +10 -0
  418. package/src/wirings/secret/validate-secret-definitions.test.ts +23 -0
  419. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  420. package/src/wirings/variable/validate-variable-definitions.test.ts +23 -0
  421. package/src/wirings/variable/validate-variable-definitions.ts +2 -0
  422. package/src/wirings/variable/variable.types.ts +9 -0
  423. package/src/wirings/virtual-user/index.ts +1 -4
  424. package/src/wirings/virtual-user/prepare-virtual-user-run.ts +1 -1
  425. package/src/wirings/virtual-user/virtual-user-derive.test.ts +1 -1
  426. package/src/wirings/virtual-user/virtual-user-derive.ts +1 -1
  427. package/src/wirings/virtual-user/virtual-user.types.ts +1 -5
  428. package/src/wirings/workflow/graph/graph-foreach-queued.test.ts +569 -0
  429. package/src/wirings/workflow/graph/graph-foreach.test.ts +626 -0
  430. package/src/wirings/workflow/graph/graph-node.ts +16 -1
  431. package/src/wirings/workflow/graph/graph-runner.ts +387 -61
  432. package/src/wirings/workflow/graph/workflow-graph.types.ts +28 -3
  433. package/src/wirings/workflow/index.ts +8 -0
  434. package/src/wirings/workflow/pikku-scenario-service.ts +59 -19
  435. package/src/wirings/workflow/pikku-workflow-service.ts +6 -8
  436. package/src/wirings/workflow/run-timeline.ts +1 -1
  437. package/src/wirings/workflow/scenario-expectations.test.ts +28 -13
  438. package/src/wirings/workflow/scenario-run.types.ts +186 -0
  439. package/src/wirings/workflow/scenario-step-guards.ts +1 -14
  440. package/src/wirings/workflow/scenario-step.test.ts +32 -7
  441. package/src/wirings/workflow/scenario-step.types.ts +27 -12
  442. package/src/wirings/workflow/workflow-approval-policy.test.ts +117 -47
  443. package/src/wirings/workflow/workflow-approval.ts +2 -1
  444. package/src/wirings/workflow/workflow-run-engine.types.ts +2 -1
  445. package/src/wirings/workflow/workflow.types.ts +4 -12
  446. package/src/wirings-stay-decoupled.test.ts +6 -3
  447. package/tsconfig.tsbuildinfo +1 -1
  448. package/tsconfig.type-tests.json +3 -2
  449. package/dist/ecosystem/ai-agent.d.ts +0 -18
  450. package/dist/ecosystem/ai-agent.js +0 -6
  451. package/dist/ecosystem/ai-scorer.d.ts +0 -12
  452. package/dist/ecosystem/ai-scorer.js +0 -5
  453. package/dist/services/ai-agent-runner-service.d.ts +0 -220
  454. package/dist/services/ai-run-state-service.d.ts +0 -29
  455. package/dist/services/ai-storage-service.d.ts +0 -18
  456. package/dist/services/in-memory-ai-run-state-service.d.ts +0 -19
  457. package/dist/services/in-memory-ai-run-state-service.js +0 -54
  458. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +0 -3
  459. package/dist/testing/service-tests/ai-storage-service-tests.js +0 -302
  460. package/dist/wirings/ai-agent/agent-rpc.d.ts +0 -15
  461. package/dist/wirings/ai-agent/agent-rpc.js +0 -53
  462. package/dist/wirings/ai-agent/ai-agent-agui.d.ts +0 -79
  463. package/dist/wirings/ai-agent/ai-agent-agui.js +0 -327
  464. package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +0 -58
  465. package/dist/wirings/ai-agent/ai-agent-finalize.js +0 -138
  466. package/dist/wirings/ai-agent/ai-agent-helpers.d.ts +0 -35
  467. package/dist/wirings/ai-agent/ai-agent-helpers.js +0 -47
  468. package/dist/wirings/ai-agent/ai-agent-interrupt.d.ts +0 -153
  469. package/dist/wirings/ai-agent/ai-agent-interrupt.js +0 -257
  470. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +0 -42
  471. package/dist/wirings/ai-agent/ai-agent-memory.js +0 -333
  472. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +0 -16
  473. package/dist/wirings/ai-agent/ai-agent-model-config.js +0 -51
  474. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +0 -117
  475. package/dist/wirings/ai-agent/ai-agent-prepare.js +0 -728
  476. package/dist/wirings/ai-agent/ai-agent-registry.d.ts +0 -16
  477. package/dist/wirings/ai-agent/ai-agent-registry.js +0 -51
  478. package/dist/wirings/ai-agent/ai-agent-runner.d.ts +0 -7
  479. package/dist/wirings/ai-agent/ai-agent-runner.js +0 -580
  480. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +0 -41
  481. package/dist/wirings/ai-agent/ai-agent-stream.js +0 -1029
  482. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +0 -57
  483. package/dist/wirings/ai-agent/ai-agent-turn.js +0 -82
  484. package/dist/wirings/ai-agent/ai-agent-utils.d.ts +0 -1
  485. package/dist/wirings/ai-agent/ai-agent-utils.js +0 -1
  486. package/dist/wirings/ai-agent/ai-agent.types.d.ts +0 -625
  487. package/dist/wirings/ai-agent/index.d.ts +0 -12
  488. package/dist/wirings/ai-agent/index.js +0 -10
  489. package/dist/wirings/ai-agent/voice-input.d.ts +0 -69
  490. package/dist/wirings/ai-agent/voice-input.js +0 -150
  491. package/dist/wirings/ai-agent/voice-output.d.ts +0 -85
  492. package/dist/wirings/ai-agent/voice-output.js +0 -196
  493. package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +0 -26
  494. package/dist/wirings/ai-scorer/ai-scorer-grade.js +0 -33
  495. package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +0 -17
  496. package/dist/wirings/ai-scorer/ai-scorer-judge.js +0 -92
  497. package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +0 -15
  498. package/dist/wirings/ai-scorer/ai-scorer-live.js +0 -38
  499. package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +0 -18
  500. package/dist/wirings/ai-scorer/ai-scorer-registry.js +0 -46
  501. package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +0 -8
  502. package/dist/wirings/ai-scorer/ai-scorer-sampling.js +0 -31
  503. package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +0 -10
  504. package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +0 -40
  505. package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +0 -15
  506. package/dist/wirings/ai-scorer/ai-scorer-worker.js +0 -58
  507. package/dist/wirings/ai-scorer/ai-scorer.d.ts +0 -39
  508. package/dist/wirings/ai-scorer/ai-scorer.js +0 -40
  509. package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +0 -90
  510. package/dist/wirings/ai-scorer/ai-scorer.types.js +0 -4
  511. package/dist/wirings/ai-scorer/index.d.ts +0 -6
  512. package/dist/wirings/ai-scorer/index.js +0 -5
  513. package/src/ecosystem/agent-scorer.ts +0 -24
  514. package/src/ecosystem/agent.ts +0 -42
  515. package/src/ecosystem/channel/local.ts +0 -1
  516. package/src/ecosystem/channel.ts +0 -22
  517. package/src/ecosystem/cli/channel.ts +0 -25
  518. package/src/ecosystem/cli.ts +0 -21
  519. package/src/ecosystem/credential.ts +0 -10
  520. package/src/ecosystem/dev.ts +0 -12
  521. package/src/ecosystem/function.ts +0 -28
  522. package/src/ecosystem/gateway.ts +0 -4
  523. package/src/ecosystem/hmac.ts +0 -1
  524. package/src/ecosystem/http.ts +0 -22
  525. package/src/ecosystem/mcp.ts +0 -17
  526. package/src/ecosystem/middleware.ts +0 -27
  527. package/src/ecosystem/node-host-resolver.ts +0 -1
  528. package/src/ecosystem/node.ts +0 -5
  529. package/src/ecosystem/oauth2.ts +0 -4
  530. package/src/ecosystem/persona.ts +0 -30
  531. package/src/ecosystem/queue.ts +0 -23
  532. package/src/ecosystem/remote.ts +0 -9
  533. package/src/ecosystem/role.ts +0 -13
  534. package/src/ecosystem/rpc.ts +0 -7
  535. package/src/ecosystem/safe-fetch.ts +0 -8
  536. package/src/ecosystem/scenario.ts +0 -32
  537. package/src/ecosystem/scheduler.ts +0 -5
  538. package/src/ecosystem/schema.ts +0 -1
  539. package/src/ecosystem/scope.ts +0 -14
  540. package/src/ecosystem/secret.ts +0 -15
  541. package/src/ecosystem/services/istanbul-coverage.ts +0 -1
  542. package/src/ecosystem/services/local-content-request-handler.ts +0 -16
  543. package/src/ecosystem/services/local-content.ts +0 -1
  544. package/src/ecosystem/services/v8-coverage.ts +0 -1
  545. package/src/ecosystem/services.ts +0 -82
  546. package/src/ecosystem/trigger.ts +0 -7
  547. package/src/ecosystem/types.ts +0 -34
  548. package/src/ecosystem/variable.ts +0 -14
  549. package/src/ecosystem/virtual-user.ts +0 -48
  550. package/src/ecosystem/workflow.ts +0 -68
  551. package/src/ecosystem.ts +0 -40
  552. package/src/index.ts +0 -244
  553. /package/dist/{services/ai-agent-runner-service.js → bootstrap-compat/root.js} +0 -0
  554. /package/dist/{services/ai-run-state-service.js → classification/data-classification.js} +0 -0
  555. /package/dist/{services/ai-storage-service.js → errors/serialized-error.js} +0 -0
  556. /package/dist/{wirings/ai-agent/ai-agent.types.js → function/function-meta.types.js} +0 -0
  557. /package/src/{column-form.ts → classification/column-form.ts} +0 -0
  558. /package/src/{pikku-request.test.ts → function/pikku-request.test.ts} +0 -0
  559. /package/src/{pikku-request.ts → function/pikku-request.ts} +0 -0
  560. /package/src/wirings/{rpc → addon}/addon-auth-tags.test.ts +0 -0
  561. /package/src/wirings/{rpc → addon}/addon-runner.ts +0 -0
  562. /package/src/wirings/{rpc → addon}/addon-secrets.test.ts +0 -0
  563. /package/src/wirings/{rpc → addon}/wire-addon.test.ts +0 -0
  564. /package/src/wirings/{rpc → addon}/wire-remote-addon.ts +0 -0
@@ -23,11 +23,25 @@ function buildTemplateRegex(nodeId) {
23
23
  .join('.+');
24
24
  return new RegExp(`^${escaped}$`);
25
25
  }
26
- function stripInstanceOrdinal(name) {
27
- const hash = name.lastIndexOf('#');
26
+ const FANOUT_INSTANCE = /^(.+)\[(\d+)\]$/;
27
+ function fanoutInstanceKey(base, index) {
28
+ return `${base}[${index}]`;
29
+ }
30
+ function splitFanoutInstance(name) {
31
+ const match = FANOUT_INSTANCE.exec(name);
32
+ if (!match)
33
+ return null;
34
+ return { base: match[1], index: Number(match[2]) };
35
+ }
36
+ export function stripInstanceOrdinal(name) {
37
+ const fanout = splitFanoutInstance(name);
38
+ const withoutItem = fanout ? fanout.base : name;
39
+ const hash = withoutItem.lastIndexOf('#');
28
40
  if (hash <= 0)
29
- return name;
30
- return /^\d+$/.test(name.slice(hash + 1)) ? name.slice(0, hash) : name;
41
+ return withoutItem;
42
+ return /^\d+$/.test(withoutItem.slice(hash + 1))
43
+ ? withoutItem.slice(0, hash)
44
+ : withoutItem;
31
45
  }
32
46
  function remapStepNamesToNodeIds(stepNames, nodes, graphName) {
33
47
  const templatePatterns = new Map();
@@ -72,40 +86,123 @@ function closesCycle(source, target, nodes) {
72
86
  }
73
87
  return false;
74
88
  }
75
- function planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, graphName) {
89
+ function planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, graphName, fanoutWidths = {}) {
76
90
  const toLogical = (name) => remapStepNamesToNodeIds([name], nodes, graphName)[0];
77
91
  const countByLogical = {};
92
+ const instancesByLogical = {};
78
93
  const consumed = new Set();
79
94
  for (const inst of instances) {
80
95
  const logical = toLogical(inst.stepName);
81
96
  countByLogical[logical] = (countByLogical[logical] ?? 0) + 1;
97
+ (instancesByLogical[logical] ??= []).push(inst);
82
98
  consumed.add(`${inst.fromStepName ?? ENTRY_FROM}->${logical}`);
83
99
  }
100
+ const isFanned = (nodeId) => Boolean(nodes[nodeId]?.forEach);
101
+ const fanoutComplete = (nodeId) => {
102
+ const width = fanoutWidths[nodeId];
103
+ if (width === undefined)
104
+ return false;
105
+ const succeeded = (instancesByLogical[nodeId] ?? []).filter((i) => i.status === 'succeeded').length;
106
+ return succeeded >= width;
107
+ };
84
108
  const completed = instances.filter((i) => i.status === 'succeeded');
85
- const completedLogical = new Set(completed.map((i) => toLogical(i.stepName)));
109
+ const completedLogical = new Set();
110
+ for (const inst of completed) {
111
+ const logical = toLogical(inst.stepName);
112
+ if (!isFanned(logical))
113
+ completedLogical.add(logical);
114
+ }
115
+ for (const nodeId of Object.keys(fanoutWidths)) {
116
+ if (isFanned(nodeId) && fanoutComplete(nodeId))
117
+ completedLogical.add(nodeId);
118
+ }
86
119
  const edges = [];
87
120
  for (const entryId of entryNodeIds) {
88
121
  edges.push({ fromKey: ENTRY_FROM, target: entryId });
89
122
  }
123
+ const fannedEdgesEmitted = new Set();
90
124
  for (const inst of completed) {
91
125
  const fromLogical = toLogical(inst.stepName);
92
126
  const node = nodes[fromLogical];
93
127
  if (!node?.next)
94
128
  continue;
95
- for (const target of resolveNextFromConfig(node.next, branchByStep[inst.stepName])) {
96
- edges.push({
97
- from: inst.stepName,
98
- fromKey: inst.stepName,
99
- fromLogical,
100
- target,
101
- });
129
+ let from = inst.stepName;
130
+ let branchKey = branchByStep[inst.stepName];
131
+ if (isFanned(fromLogical)) {
132
+ if (!completedLogical.has(fromLogical))
133
+ continue;
134
+ if (fannedEdgesEmitted.has(fromLogical))
135
+ continue;
136
+ fannedEdgesEmitted.add(fromLogical);
137
+ from = fromLogical;
138
+ branchKey = branchByStep[fromLogical] ?? branchByStep[inst.stepName];
139
+ }
140
+ for (const target of resolveNextFromConfig(node.next, branchKey)) {
141
+ edges.push({ from, fromKey: from, fromLogical, target });
142
+ }
143
+ }
144
+ // A zero-width fanout produces no instances at all, so its outgoing edges
145
+ // have no completed instance to hang off — emit them from the logical node.
146
+ for (const [nodeId, width] of Object.entries(fanoutWidths)) {
147
+ if (width !== 0 || fannedEdgesEmitted.has(nodeId))
148
+ continue;
149
+ const node = nodes[nodeId];
150
+ if (!node?.next)
151
+ continue;
152
+ fannedEdgesEmitted.add(nodeId);
153
+ for (const target of resolveNextFromConfig(node.next, branchByStep[nodeId])) {
154
+ edges.push({ from: nodeId, fromKey: nodeId, fromLogical: nodeId, target });
102
155
  }
103
156
  }
104
157
  const toFire = [];
158
+ const plannedFanout = new Set();
105
159
  let blockedWaiting = false;
106
160
  for (const edge of edges) {
107
161
  const target = edge.target;
108
162
  const edgeKey = `${edge.fromKey}->${target}`;
163
+ if (isFanned(target)) {
164
+ if (plannedFanout.has(target))
165
+ continue;
166
+ plannedFanout.add(target);
167
+ if (!areDependenciesSatisfied(nodes[target] ?? {}, completedLogical)) {
168
+ blockedWaiting = true;
169
+ continue;
170
+ }
171
+ const width = fanoutWidths[target];
172
+ if (width === undefined) {
173
+ blockedWaiting = true;
174
+ continue;
175
+ }
176
+ const existing = instancesByLogical[target] ?? [];
177
+ const existingIndexes = new Set(existing
178
+ .map((i) => splitFanoutInstance(i.stepName)?.index)
179
+ .filter((index) => index !== undefined));
180
+ const pending = [];
181
+ for (let index = 0; index < width; index++) {
182
+ if (!existingIndexes.has(index))
183
+ pending.push(index);
184
+ }
185
+ if (pending.length === 0)
186
+ continue;
187
+ const sequential = nodes[target]?.mode === 'sequential';
188
+ const anyInFlight = existing.some((i) => i.status !== 'succeeded');
189
+ const emit = sequential
190
+ ? anyInFlight
191
+ ? []
192
+ : pending.slice(0, 1)
193
+ : pending;
194
+ for (const index of emit) {
195
+ toFire.push({
196
+ logical: target,
197
+ instanceKey: fanoutInstanceKey(target, index),
198
+ fromStepName: edge.from,
199
+ itemIndex: index,
200
+ });
201
+ }
202
+ if (emit.length === 0)
203
+ blockedWaiting = true;
204
+ continue;
205
+ }
109
206
  if (consumed.has(edgeKey))
110
207
  continue;
111
208
  const visits = countByLogical[target] ?? 0;
@@ -237,7 +334,7 @@ function resolveTemplate(template, nodeResults) {
237
334
  }
238
335
  function resolveValue(value, nodeResults) {
239
336
  if (isDataRef(value)) {
240
- if (value.$ref === '$item') {
337
+ if (value.$ref === '$item' && !('$item' in nodeResults)) {
241
338
  return value;
242
339
  }
243
340
  const source = nodeResults[value.$ref];
@@ -297,6 +394,7 @@ function extractReferencedNodeIds(input) {
297
394
  return [...new Set(nodeIds)];
298
395
  }
299
396
  const IGNORED_REFS = new Set(['trigger', '$item', 'unknown']);
397
+ const FOREACH_MODES = new Set(['parallel', 'sequential']);
300
398
  function normalizeNodeTargets(value) {
301
399
  if (!value)
302
400
  return [];
@@ -327,6 +425,21 @@ function validateGraphReferences(graphName, nodes, entryNodes) {
327
425
  throw new Error(`Workflow graph '${graphName}': node '${nodeId}' references unknown node '${refId}' in input`);
328
426
  }
329
427
  }
428
+ if (node.forEach !== undefined) {
429
+ const source = forEachSource(node);
430
+ if (!source) {
431
+ throw new Error(`Workflow graph '${graphName}': node '${nodeId}' forEach must reference a node`);
432
+ }
433
+ if (!IGNORED_REFS.has(source.$ref) && !nodeIds.has(source.$ref)) {
434
+ throw new Error(`Workflow graph '${graphName}': node '${nodeId}' references unknown node '${source.$ref}' in forEach`);
435
+ }
436
+ if (node.mode !== undefined && !FOREACH_MODES.has(node.mode)) {
437
+ throw new Error(`Workflow graph '${graphName}': node '${nodeId}' has an unknown forEach mode '${node.mode}'`);
438
+ }
439
+ }
440
+ else if (node.mode !== undefined) {
441
+ throw new Error(`Workflow graph '${graphName}': node '${nodeId}' sets mode '${node.mode}' without a forEach`);
442
+ }
330
443
  const nextTargets = normalizeNodeTargets(node.next);
331
444
  for (const nextId of nextTargets) {
332
445
  if (!nodeIds.has(nextId)) {
@@ -341,9 +454,114 @@ function validateGraphReferences(graphName, nodes, entryNodes) {
341
454
  }
342
455
  }
343
456
  }
457
+ function forEachSource(node) {
458
+ return isDataRef(node.forEach) ? node.forEach : undefined;
459
+ }
460
+ function nodeDependencies(node) {
461
+ const deps = extractReferencedNodeIds(node.input);
462
+ const source = forEachSource(node);
463
+ if (source)
464
+ deps.push(source.$ref);
465
+ return [...new Set(deps)].filter((id) => !IGNORED_REFS.has(id));
466
+ }
467
+ function describeValue(value) {
468
+ if (value === null)
469
+ return 'null';
470
+ if (value === undefined)
471
+ return 'undefined';
472
+ return typeof value;
473
+ }
474
+ function resolveForEachItems(graphName, nodeId, node, nodeResults) {
475
+ const source = forEachSource(node);
476
+ if (!source) {
477
+ throw new Error(`Workflow graph '${graphName}': node '${nodeId}' has an unusable forEach configuration`);
478
+ }
479
+ const raw = nodeResults[source.$ref];
480
+ const value = source.path ? getValueAtPath(raw, source.path) : raw;
481
+ if (!Array.isArray(value)) {
482
+ const label = source.path ? `${source.$ref}.${source.path}` : source.$ref;
483
+ throw new Error(`Workflow graph '${graphName}': node '${nodeId}' forEach source '${label}' resolved to ${describeValue(value)}, expected an array`);
484
+ }
485
+ return value;
486
+ }
344
487
  function areDependenciesSatisfied(node, completedNodeIds) {
345
- const deps = extractReferencedNodeIds(node.input).filter((id) => !IGNORED_REFS.has(id));
346
- return deps.every((dep) => completedNodeIds.has(dep));
488
+ return nodeDependencies(node).every((dep) => completedNodeIds.has(dep));
489
+ }
490
+ async function createGraphResultReader(workflowService, runId, graphName, nodes, triggerInput, instances) {
491
+ const succeeded = new Set(instances.filter((i) => i.status === 'succeeded').map((i) => i.stepName));
492
+ const fanoutItems = {};
493
+ const fanoutWidths = {};
494
+ const resultCache = new Map();
495
+ const resolving = new Set();
496
+ const readResult = async (nodeId) => {
497
+ if (nodeId === 'trigger')
498
+ return triggerInput;
499
+ if (resultCache.has(nodeId))
500
+ return resultCache.get(nodeId);
501
+ if (resolving.has(nodeId))
502
+ return undefined;
503
+ resolving.add(nodeId);
504
+ try {
505
+ let value;
506
+ if (nodes[nodeId]?.forEach) {
507
+ const items = await readFanoutItems(nodeId);
508
+ if (items === undefined)
509
+ return undefined;
510
+ const keys = items.map((_, index) => fanoutInstanceKey(nodeId, index));
511
+ if (keys.some((key) => !succeeded.has(key)))
512
+ return undefined;
513
+ const fetched = keys.length
514
+ ? await workflowService.getNodeResults(runId, keys)
515
+ : {};
516
+ value = keys.map((key) => fetched[key]);
517
+ }
518
+ else {
519
+ if (!succeeded.has(nodeId))
520
+ return undefined;
521
+ const fetched = await workflowService.getNodeResults(runId, [nodeId]);
522
+ value = fetched[nodeId];
523
+ }
524
+ resultCache.set(nodeId, value);
525
+ return value;
526
+ }
527
+ finally {
528
+ resolving.delete(nodeId);
529
+ }
530
+ };
531
+ const readFanoutItems = async (nodeId) => {
532
+ if (fanoutItems[nodeId])
533
+ return fanoutItems[nodeId];
534
+ const node = nodes[nodeId];
535
+ const source = forEachSource(node);
536
+ if (!source)
537
+ return undefined;
538
+ const sourceValue = await readResult(source.$ref);
539
+ if (sourceValue === undefined)
540
+ return undefined;
541
+ const items = resolveForEachItems(graphName, nodeId, node, {
542
+ [source.$ref]: sourceValue,
543
+ });
544
+ fanoutItems[nodeId] = items;
545
+ fanoutWidths[nodeId] = items.length;
546
+ return items;
547
+ };
548
+ for (const nodeId of Object.keys(nodes)) {
549
+ if (nodes[nodeId]?.forEach)
550
+ await readFanoutItems(nodeId);
551
+ }
552
+ return {
553
+ fanoutItems,
554
+ fanoutWidths,
555
+ read: async (nodeIds) => {
556
+ const results = {};
557
+ for (const nodeId of nodeIds) {
558
+ const value = await readResult(nodeId);
559
+ if (value !== undefined)
560
+ results[nodeId] = value;
561
+ }
562
+ return results;
563
+ },
564
+ };
347
565
  }
348
566
  async function queueGraphNode(workflowService, runId, _graphName, nodeId, rpcName, input, nodeConfig, fromStepName) {
349
567
  const stepOptions = {
@@ -378,21 +596,28 @@ export async function continueGraph(workflowService, runId, graphName, overrideM
378
596
  return;
379
597
  }
380
598
  const instances = await workflowService.getStepInstances(runId);
381
- const plan = planGraphTransitions(nodes, instances, branchByStep, meta.entryNodeIds ?? [], graphName);
599
+ const triggerInput = currentRun?.input;
600
+ const reader = await createGraphResultReader(workflowService, runId, graphName, nodes, triggerInput, instances);
601
+ const plan = planGraphTransitions(nodes, instances, branchByStep, meta.entryNodeIds ?? [], graphName, reader.fanoutWidths);
382
602
  if (plan.toFire.length === 0) {
383
603
  if (!plan.hasInFlight && !plan.blockedWaiting) {
384
604
  await workflowService.updateRunStatus(runId, 'completed');
385
605
  }
386
606
  return;
387
607
  }
388
- const triggerInput = currentRun?.input;
389
608
  for (const fire of plan.toFire) {
390
609
  const node = nodes[fire.logical];
391
610
  if (!node?.rpcName)
392
611
  continue;
393
612
  const referencedNodeIds = extractReferencedNodeIds(node.input).filter((id) => !IGNORED_REFS.has(id));
394
- const fetchedResults = await workflowService.getNodeResults(runId, referencedNodeIds);
395
- const nodeResults = { trigger: triggerInput, ...fetchedResults };
613
+ const fetchedResults = await reader.read(referencedNodeIds);
614
+ const nodeResults = {
615
+ trigger: triggerInput,
616
+ ...fetchedResults,
617
+ };
618
+ if (fire.itemIndex !== undefined) {
619
+ nodeResults['$item'] = reader.fanoutItems[fire.logical]?.[fire.itemIndex];
620
+ }
396
621
  const resolvedInput = resolveSerializedInput(node.input, nodeResults);
397
622
  await queueGraphNode(workflowService, runId, graphName, fire.instanceKey, node.rpcName, resolvedInput, node, fire.fromStepName);
398
623
  }
@@ -579,7 +804,8 @@ async function continueGraphInline(workflowService, rpcService, runId, graphName
579
804
  return;
580
805
  }
581
806
  const instances = await workflowService.getStepInstances(runId);
582
- const plan = planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, graphName);
807
+ const reader = await createGraphResultReader(workflowService, runId, graphName, nodes, triggerInput, instances);
808
+ const plan = planGraphTransitions(nodes, instances, branchByStep, entryNodeIds, graphName, reader.fanoutWidths);
583
809
  if (plan.toFire.length === 0) {
584
810
  if (!plan.hasInFlight && !plan.blockedWaiting) {
585
811
  await workflowService.updateRunStatus(runId, 'completed');
@@ -592,8 +818,15 @@ async function continueGraphInline(workflowService, rpcService, runId, graphName
592
818
  if (!node?.rpcName)
593
819
  return;
594
820
  const referencedNodeIds = extractReferencedNodeIds(node.input).filter((id) => !IGNORED_REFS.has(id));
595
- const fetchedResults = await workflowService.getNodeResults(runId, referencedNodeIds);
596
- const nodeResults = { trigger: triggerInput, ...fetchedResults };
821
+ const fetchedResults = await reader.read(referencedNodeIds);
822
+ const nodeResults = {
823
+ trigger: triggerInput,
824
+ ...fetchedResults,
825
+ };
826
+ if (fire.itemIndex !== undefined) {
827
+ nodeResults['$item'] =
828
+ reader.fanoutItems[fire.logical]?.[fire.itemIndex];
829
+ }
597
830
  const resolvedInput = resolveSerializedInput(node.input, nodeResults);
598
831
  executed++;
599
832
  await executeGraphNodeInline(workflowService, rpcService, runId, graphName, fire.logical, fire.instanceKey, resolvedInput, nodes, fire.fromStepName);
@@ -602,6 +835,27 @@ async function continueGraphInline(workflowService, rpcService, runId, graphName
602
835
  return;
603
836
  }
604
837
  }
838
+ function planEntryFirings(graphName, nodeId, node, triggerNodeResults, triggerInput) {
839
+ if (!node.forEach) {
840
+ const input = node.input && Object.keys(node.input).length > 0
841
+ ? resolveSerializedInput(node.input, triggerNodeResults)
842
+ : triggerInput;
843
+ return [{ instanceKey: nodeId, input }];
844
+ }
845
+ const items = resolveForEachItems(graphName, nodeId, node, triggerNodeResults);
846
+ const width = node.mode === 'sequential' ? Math.min(items.length, 1) : items.length;
847
+ const firings = [];
848
+ for (let index = 0; index < width; index++) {
849
+ firings.push({
850
+ instanceKey: fanoutInstanceKey(nodeId, index),
851
+ input: resolveSerializedInput(node.input, {
852
+ ...triggerNodeResults,
853
+ $item: items[index],
854
+ }),
855
+ });
856
+ }
857
+ return firings;
858
+ }
605
859
  export async function runWorkflowGraph(workflowService, graphName, triggerInput, rpcService, inline, startNode, wire, overrideMeta) {
606
860
  const meta = overrideMeta ?? getWorkflowMeta(graphName);
607
861
  if (!meta?.nodes) {
@@ -643,10 +897,8 @@ export async function runWorkflowGraph(workflowService, graphName, triggerInput,
643
897
  const node = nodes[nodeId];
644
898
  if (!node?.rpcName)
645
899
  return;
646
- const resolvedInput = node.input && Object.keys(node.input).length > 0
647
- ? resolveSerializedInput(node.input, triggerNodeResults)
648
- : triggerInput;
649
- await executeGraphNodeInline(workflowService, rpcService, runId, graphName, nodeId, nodeId, resolvedInput, nodes);
900
+ const firings = planEntryFirings(graphName, nodeId, node, triggerNodeResults, triggerInput);
901
+ await Promise.all(firings.map((firing) => executeGraphNodeInline(workflowService, rpcService, runId, graphName, nodeId, firing.instanceKey, firing.input, nodes)));
650
902
  }));
651
903
  await continueGraphInline(workflowService, rpcService, runId, graphName, nodes, triggerInput, entryNodes);
652
904
  }
@@ -672,10 +924,10 @@ export async function runWorkflowGraph(workflowService, graphName, triggerInput,
672
924
  const node = nodes[nodeId];
673
925
  if (!node?.rpcName)
674
926
  continue;
675
- const resolvedInput = node.input && Object.keys(node.input).length > 0
676
- ? resolveSerializedInput(node.input, triggerNodeResults)
677
- : triggerInput;
678
- await queueGraphNode(workflowService, runId, graphName, nodeId, node.rpcName, resolvedInput, node);
927
+ const firings = planEntryFirings(graphName, nodeId, node, triggerNodeResults, triggerInput);
928
+ for (const firing of firings) {
929
+ await queueGraphNode(workflowService, runId, graphName, firing.instanceKey, node.rpcName, firing.input, node);
930
+ }
679
931
  }
680
932
  if (inline) {
681
933
  workflowService.unregisterInlineRun(runId);
@@ -1,3 +1,4 @@
1
+ import type { TemplateString } from './template.js';
1
2
  export interface RefValue {
2
3
  __isRef: true;
3
4
  nodeId: string;
@@ -7,9 +8,23 @@ export declare const createRef: (nodeId: string, path?: string) => RefValue;
7
8
  export declare const isRef: (value: unknown) => value is RefValue;
8
9
  export type RefFn<NodeIds extends string = string> = (nodeId: NodeIds, path?: string) => RefValue;
9
10
  export type NextConfig<NodeIds extends string = string> = NodeIds | NodeIds[] | Record<string, NodeIds | NodeIds[]>;
11
+ export type TemplateFn = (templateStr: string, refs: Array<{
12
+ $ref: string;
13
+ path?: string;
14
+ }>) => TemplateString;
15
+ export type ItemFn = (path?: string) => RefValue;
16
+ export type ForEachConfig<NodeIds extends string = string> = NodeIds | RefValue | ((ref: RefFn<NodeIds>) => RefValue);
17
+ export type ForEachMode = 'parallel' | 'sequential';
10
18
  export interface GraphNodeConfig<NodeIds extends string = string> {
11
19
  func: string;
12
- input?: (ref: RefFn<NodeIds>) => Record<string, unknown>;
20
+ /**
21
+ * Run this node once per element of an upstream array. The node's own result
22
+ * becomes the ordered array of per-item results.
23
+ */
24
+ forEach?: ForEachConfig<NodeIds>;
25
+ /** How the per-item instances of a `forEach` node run. Defaults to 'parallel'. */
26
+ mode?: ForEachMode;
27
+ input?: (ref: RefFn<NodeIds>, template: TemplateFn, $item: ItemFn) => Record<string, unknown>;
13
28
  next?: NextConfig<NodeIds>;
14
29
  onError?: NodeIds | NodeIds[];
15
30
  retries?: number;
@@ -15,3 +15,5 @@ export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGr
15
15
  export type { WorkflowStepInput as WorkflowStepQueueInput, PikkuWorkflowOrchestratorInput, PikkuWorkflowSleeperInput, } from './workflow-queue-workers.js';
16
16
  export type { WorkflowService, WorkflowQueueOptions, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowsRuntimeMeta, } from './workflow.types.js';
17
17
  export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, Condition, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './workflow.types.js';
18
+ export { createGraph } from './graph/graph-node.js';
19
+ export type { GraphNodeConfig, ForEachConfig, ForEachMode, ItemFn, TemplateFn, } from './graph/workflow-graph.types.js';
@@ -9,3 +9,4 @@ export { buildRunTimeline, reconstructStateAt, reconstructFinalState, } from './
9
9
  export { addWorkflow } from './dsl/workflow-runner.js';
10
10
  export { template } from './graph/template.js';
11
11
  export { pikkuWorkflowGraph, } from './graph/wire-workflow-graph.js';
12
+ export { createGraph } from './graph/graph-node.js';
@@ -7,13 +7,14 @@ import type { ScenarioBrowserProvider, ScenarioEnvironment, ScenarioSurface } fr
7
7
  import type { PikkuWorkflowWire, WorkflowQueueOptions } from './workflow.types.js';
8
8
  export { addFeature, resolveFeatureScenarios } from './feature.js';
9
9
  export type * from './scenario.types.js';
10
+ export type * from './scenario-run.types.js';
10
11
  export { SCENARIO_SURFACES } from './scenario-step.types.js';
11
12
  export { resolveScenarioSurfaces } from './scenario-surface.js';
12
13
  export { pollUntil, type PollOptions } from './scenario-poll.js';
13
14
  export { createCookieJar } from './scenario-cookie-jar.js';
14
15
  export type { ScenarioCookieJar } from './scenario-cookie-jar.js';
15
16
  export { composeStepProse, renderStepTemplate } from './scenario-prose.js';
16
- export { requireActor, requireScenarioEnv } from './scenario-step-guards.js';
17
+ export { requireScenarioEnv } from './scenario-step-guards.js';
17
18
  export type { ScenarioHttpResponse, ScenarioJsonRequest, } from '../../services/personas-service.js';
18
19
  /**
19
20
  * A workflow service with the scenario capability attached — the two lines
@@ -72,10 +73,14 @@ export declare class ScenarioBrowserUnavailable extends PikkuError {
72
73
  constructor(stepFunc: string);
73
74
  }
74
75
  /**
75
- * A browser binding was reached without an actor, so there is no persona whose
76
- * window it would run in.
76
+ * A step that is driven by a persona was dispatched without one.
77
+ *
78
+ * Raised before the body runs rather than partway through it: the step declared
79
+ * that it needs an actor — by having a `browser` binding, which cannot be
80
+ * provisioned without a window to open, or by `actor: true` — so a call site
81
+ * that omits one is a mistake at the call site.
77
82
  */
78
- export declare class ScenarioBrowserActorRequired extends PikkuError {
83
+ export declare class ScenarioActorRequired extends PikkuError {
79
84
  readonly stepFunc: string;
80
85
  constructor(stepFunc: string);
81
86
  }
@@ -224,4 +229,9 @@ export declare class PikkuScenarioService implements WorkflowRunExtension {
224
229
  * treated as server-side only, which is what an ordinary function is.
225
230
  */
226
231
  private declaredSurfaces;
232
+ /**
233
+ * Whether a step is driven by a persona, and so must be given one. Stamped by
234
+ * the definer from a `browser` binding or an explicit `actor: true`.
235
+ */
236
+ private requiresActor;
227
237
  }
@@ -7,6 +7,21 @@ import { InMemoryWorkflowService } from '../../services/in-memory-workflow-servi
7
7
  import { runScheduledTask } from '../scheduler/scheduler-runner.js';
8
8
  import { WorkflowStepNameNotString } from './workflow-errors.js';
9
9
  import { resolveScenarioSurfaces, witnessesAgree } from './scenario-surface.js';
10
+ /**
11
+ * The retry default for the `expect*` family: none, the same as `given`,
12
+ * `when` and `then` take, and for the same reason — a failed assertion is a
13
+ * result, not a transient fault.
14
+ *
15
+ * Left to the workflow-wide `DEFAULT_STEP_RETRIES` they get six attempts each,
16
+ * which re-runs the RPC behind a failing `expectError`, restarts a whole
17
+ * `expectEventually` poll after it has already spent its own deadline, and
18
+ * lets a live judge re-roll a grade until one lands inside the band. The last
19
+ * of those is how it was found: a judge scored a deliberately useless answer
20
+ * 1, and the scenario went green anyway on the next attempt.
21
+ *
22
+ * A caller that genuinely wants attempts still asks for them.
23
+ */
24
+ const assertionStep = (options) => ({ ...options, retries: options?.retries ?? 0 });
10
25
  export { addFeature, resolveFeatureScenarios } from './feature.js';
11
26
  export { SCENARIO_SURFACES } from './scenario-step.types.js';
12
27
  // Which of a step's bindings run: one for an action, every witness for a `then`
@@ -14,7 +29,7 @@ export { resolveScenarioSurfaces } from './scenario-surface.js';
14
29
  export { pollUntil } from './scenario-poll.js';
15
30
  export { createCookieJar } from './scenario-cookie-jar.js';
16
31
  export { composeStepProse, renderStepTemplate } from './scenario-prose.js';
17
- export { requireActor, requireScenarioEnv } from './scenario-step-guards.js';
32
+ export { requireScenarioEnv } from './scenario-step-guards.js';
18
33
  // The readers themselves live on `@pikku/core/persona`
19
34
  /**
20
35
  * A workflow service with the scenario capability attached — the two lines
@@ -90,20 +105,24 @@ addError(ScenarioBrowserUnavailable, {
90
105
  message: 'No browser provider is registered.',
91
106
  });
92
107
  /**
93
- * A browser binding was reached without an actor, so there is no persona whose
94
- * window it would run in.
108
+ * A step that is driven by a persona was dispatched without one.
109
+ *
110
+ * Raised before the body runs rather than partway through it: the step declared
111
+ * that it needs an actor — by having a `browser` binding, which cannot be
112
+ * provisioned without a window to open, or by `actor: true` — so a call site
113
+ * that omits one is a mistake at the call site.
95
114
  */
96
- export class ScenarioBrowserActorRequired extends PikkuError {
115
+ export class ScenarioActorRequired extends PikkuError {
97
116
  stepFunc;
98
117
  constructor(stepFunc) {
99
- super(`[scenario] step '${stepFunc}' resolved to its browser binding but was called without an actor. ` +
100
- `Pass { actor: actors.<name> } so the browser signs in as that persona.`);
118
+ super(`[scenario] step '${stepFunc}' is driven by a persona but was called without an actor. ` +
119
+ `Pass { actor: actors.<name> } so it runs as that persona.`);
101
120
  this.stepFunc = stepFunc;
102
121
  }
103
122
  }
104
- addError(ScenarioBrowserActorRequired, {
123
+ addError(ScenarioActorRequired, {
105
124
  status: 500,
106
- message: 'A browser binding needs an actor.',
125
+ message: 'This step needs an actor.',
107
126
  });
108
127
  /**
109
128
  * A step declares no binding the run can execute.
@@ -433,7 +452,7 @@ export class PikkuScenarioService {
433
452
  }
434
453
  await new Promise((resolve) => setTimeout(resolve, interval));
435
454
  }
436
- }, options);
455
+ }, assertionStep(options));
437
456
  },
438
457
  expectError: async (stepName, rpcName, data, options) => {
439
458
  this.engine.verifyStepName(stepName);
@@ -460,7 +479,7 @@ export class PikkuScenarioService {
460
479
  return message;
461
480
  }
462
481
  throw new Error(`[workflow] expectError '${stepName}' ('${resolvedRpcName}') expected an error but the call succeeded: ${JSON.stringify(result)?.slice(0, 300)}`);
463
- }, options);
482
+ }, assertionStep(options));
464
483
  },
465
484
  expectService: async (stepName, serviceMethod, options) => {
466
485
  this.engine.verifyStepName(stepName);
@@ -489,7 +508,7 @@ export class PikkuScenarioService {
489
508
  throw new Error(`[workflow] expectService '${stepName}' expected ${expected ?? 'at least one'} call(s) to '${serviceMethod}'` +
490
509
  `${options?.calledWith !== undefined ? ` with ${JSON.stringify(options.calledWith)}` : ''}, found ${matching.length}. Recorded:\n ${seen}`);
491
510
  }
492
- }, options);
511
+ }, assertionStep(options));
493
512
  },
494
513
  expectScore: async (stepName, agentRunId, scorerName, options) => {
495
514
  this.engine.verifyStepName(stepName);
@@ -518,7 +537,7 @@ export class PikkuScenarioService {
518
537
  `${grade.reason ? `: ${grade.reason}` : ''}`);
519
538
  }
520
539
  return grade;
521
- }, options);
540
+ }, assertionStep(options));
522
541
  },
523
542
  // knowledge: decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md
524
543
  given: (stepName, stepFunc, data, options) => this.scenarioStep('given', scenarioStepContext(), stepName, stepFunc, data, options),
@@ -546,6 +565,9 @@ export class PikkuScenarioService {
546
565
  const description = options?.description ??
547
566
  this.scenarioStepDescription(packageName, resolvedStepFunc) ??
548
567
  stepName;
568
+ if (!actor && this.requiresActor(packageName, resolvedStepFunc)) {
569
+ throw new ScenarioActorRequired(resolvedStepFunc);
570
+ }
549
571
  const declared = this.declaredSurfaces(packageName, resolvedStepFunc);
550
572
  const resolution = resolveScenarioSurfaces(phase, declared, this.runSurface);
551
573
  return await this.engine.inlineStep(runId, stepName, async () => {
@@ -556,13 +578,13 @@ export class PikkuScenarioService {
556
578
  scenario: workflowWire,
557
579
  pikkuUserId: workflowWire.pikkuUserId,
558
580
  actors: this.runActors.get(runId),
581
+ ...(actor ? { actor } : {}),
559
582
  scenarioStep: {
560
583
  name: resolvedStepFunc,
561
584
  stepName,
562
585
  runId,
563
586
  phase,
564
587
  surface,
565
- actor,
566
588
  env: this.scenarioEnvironment,
567
589
  },
568
590
  };
@@ -570,9 +592,8 @@ export class PikkuScenarioService {
570
592
  if (!this.scenarioBrowserProvider) {
571
593
  throw new ScenarioBrowserUnavailable(resolvedStepFunc);
572
594
  }
573
- if (!actor) {
574
- throw new ScenarioBrowserActorRequired(resolvedStepFunc);
575
- }
595
+ // The dispatch guard above already refused an actor-less call, and a
596
+ // browser binding always requires one.
576
597
  wire.browser = await this.scenarioBrowserProvider.sessionFor(actor.name);
577
598
  }
578
599
  return await runPikkuFunc('workflow', workflowName, resolvedStepFunc, {
@@ -640,4 +661,11 @@ export class PikkuScenarioService {
640
661
  const surfaces = this.scenarioStepConfig(packageName, stepFunc)?.surfaces;
641
662
  return surfaces?.length ? surfaces : ['default'];
642
663
  }
664
+ /**
665
+ * Whether a step is driven by a persona, and so must be given one. Stamped by
666
+ * the definer from a `browser` binding or an explicit `actor: true`.
667
+ */
668
+ requiresActor(packageName, stepFunc) {
669
+ return (this.scenarioStepConfig(packageName, stepFunc)?.requiresActor === true);
670
+ }
643
671
  }