@vercel/factory 0.0.15 → 0.0.16

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 (214) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/README.md +49 -261
  3. package/dist/agent-routes.d.mts +47 -3
  4. package/dist/agent-routes.mjs +28 -1
  5. package/dist/agent-routes.mjs.map +1 -1
  6. package/dist/api-contracts.d.mts +20 -1
  7. package/dist/api-contracts.mjs +2 -1
  8. package/dist/api-contracts.mjs.map +1 -1
  9. package/dist/api.d.mts +45 -2
  10. package/dist/api.mjs +199 -10
  11. package/dist/api.mjs.map +1 -1
  12. package/dist/approval-contracts.d.mts +6 -0
  13. package/dist/blob/index.d.mts +51 -14
  14. package/dist/blob/index.mjs +26 -10
  15. package/dist/blob/index.mjs.map +1 -1
  16. package/dist/budget.d.mts +7 -0
  17. package/dist/budget.mjs +6 -0
  18. package/dist/budget.mjs.map +1 -1
  19. package/dist/build-factory.d.mts +1 -0
  20. package/dist/change-verification/dispatch.d.mts +16 -3
  21. package/dist/change-verification/dispatch.mjs +45 -7
  22. package/dist/change-verification/dispatch.mjs.map +1 -1
  23. package/dist/change-verification/eve-tool.d.mts +2 -1
  24. package/dist/change-verification/eve-tool.mjs +35 -47
  25. package/dist/change-verification/eve-tool.mjs.map +1 -1
  26. package/dist/change-verification/result.mjs +130 -0
  27. package/dist/change-verification/result.mjs.map +1 -0
  28. package/dist/changes/eve-record-change.d.mts +2 -2
  29. package/dist/changes/eve-record-change.mjs +43 -9
  30. package/dist/changes/eve-record-change.mjs.map +1 -1
  31. package/dist/changes.d.mts +4 -3
  32. package/dist/changes.mjs +2 -2
  33. package/dist/changes.mjs.map +1 -1
  34. package/dist/client-events.d.mts +10 -3
  35. package/dist/client-events.mjs +6 -2
  36. package/dist/client-events.mjs.map +1 -1
  37. package/dist/client-stream.mjs +8 -2
  38. package/dist/client-stream.mjs.map +1 -1
  39. package/dist/client-transcript.mjs +5 -1
  40. package/dist/client-transcript.mjs.map +1 -1
  41. package/dist/client.d.mts +121 -12
  42. package/dist/client.mjs +117 -9
  43. package/dist/client.mjs.map +1 -1
  44. package/dist/code-review/contracts.d.mts +1 -0
  45. package/dist/code-review/eve-post-review.d.mts +4 -4
  46. package/dist/code-review/eve-post-review.mjs +29 -17
  47. package/dist/code-review/eve-post-review.mjs.map +1 -1
  48. package/dist/code-review/eve-review-comments.d.mts +13 -2
  49. package/dist/code-review/eve-review-comments.mjs +45 -11
  50. package/dist/code-review/eve-review-comments.mjs.map +1 -1
  51. package/dist/code-review/github-reporter.d.mts +2 -0
  52. package/dist/code-review/github-reporter.mjs +7 -5
  53. package/dist/code-review/github-reporter.mjs.map +1 -1
  54. package/dist/code-review.d.mts +3 -2
  55. package/dist/deepsec/eve-tool.mjs +3 -1
  56. package/dist/deepsec/eve-tool.mjs.map +1 -1
  57. package/dist/dispatch.d.mts +79 -8
  58. package/dist/dispatch.mjs +68 -9
  59. package/dist/dispatch.mjs.map +1 -1
  60. package/dist/eve/index.d.mts +70 -10
  61. package/dist/eve/index.mjs +93 -22
  62. package/dist/eve/index.mjs.map +1 -1
  63. package/dist/eve/invoke.mjs +24 -11
  64. package/dist/eve/invoke.mjs.map +1 -1
  65. package/dist/eve/session-client.d.mts +122 -4
  66. package/dist/eve/session-client.mjs +127 -13
  67. package/dist/eve/session-client.mjs.map +1 -1
  68. package/dist/eve/task-execution.d.mts +380 -0
  69. package/dist/eve/task-execution.mjs +57 -2
  70. package/dist/eve/task-execution.mjs.map +1 -1
  71. package/dist/eve/task-session.d.mts +44 -2
  72. package/dist/eve/task-session.mjs +44 -2
  73. package/dist/eve/task-session.mjs.map +1 -1
  74. package/dist/eve/transcript.mjs +5 -1
  75. package/dist/eve/transcript.mjs.map +1 -1
  76. package/dist/execution.d.mts +117 -9
  77. package/dist/execution.mjs +76 -6
  78. package/dist/execution.mjs.map +1 -1
  79. package/dist/finding-remediation/admission.d.mts +2 -0
  80. package/dist/finding-remediation/admission.mjs +4 -1
  81. package/dist/finding-remediation/admission.mjs.map +1 -1
  82. package/dist/findings.d.mts +1 -0
  83. package/dist/github-publication.d.mts +1 -0
  84. package/dist/github-publication.mjs +97 -84
  85. package/dist/github-publication.mjs.map +1 -1
  86. package/dist/github-transfer.d.mts +15 -6
  87. package/dist/github-transfer.mjs +214 -66
  88. package/dist/github-transfer.mjs.map +1 -1
  89. package/dist/github.d.mts +51 -11
  90. package/dist/github.mjs +126 -24
  91. package/dist/github.mjs.map +1 -1
  92. package/dist/inbox-activity.d.mts +53 -0
  93. package/dist/inbox-activity.mjs +41 -0
  94. package/dist/inbox-activity.mjs.map +1 -0
  95. package/dist/index.d.mts +3 -1
  96. package/dist/index.mjs +3 -2
  97. package/dist/intake-contracts.d.mts +0 -1
  98. package/dist/integrations/deepsec.d.mts +1 -0
  99. package/dist/integrations/github.d.mts +2 -2
  100. package/dist/integrations/github.mjs +2 -2
  101. package/dist/integrations/slack.d.mts +3 -1
  102. package/dist/integrations/slack.mjs +3 -1
  103. package/dist/integrations/vercel.d.mts +4 -2
  104. package/dist/integrations/vercel.mjs +3 -2
  105. package/dist/merge-resolution/eve-tools.d.mts +1 -0
  106. package/dist/merge-resolution/eve-tools.mjs +7 -2
  107. package/dist/merge-resolution/eve-tools.mjs.map +1 -1
  108. package/dist/model-settings.d.mts +41 -0
  109. package/dist/model-settings.mjs +35 -0
  110. package/dist/model-settings.mjs.map +1 -0
  111. package/dist/planning/reconcile.mjs +6 -0
  112. package/dist/planning/reconcile.mjs.map +1 -1
  113. package/dist/postgres/index.d.mts +43 -2
  114. package/dist/postgres/index.mjs +40 -2
  115. package/dist/postgres/index.mjs.map +1 -1
  116. package/dist/presets/software-development/dispatch.d.mts +4 -1
  117. package/dist/presets/software-development/dispatch.mjs +2 -1
  118. package/dist/presets/software-development/dispatch.mjs.map +1 -1
  119. package/dist/presets/software-development/task-communication.d.mts +1 -0
  120. package/dist/presets/software-development/task-communication.mjs +48 -11
  121. package/dist/presets/software-development/task-communication.mjs.map +1 -1
  122. package/dist/presets/software-development.d.mts +1 -0
  123. package/dist/pull-requests/github-publisher.d.mts +15 -1
  124. package/dist/pull-requests/github-publisher.mjs +61 -1
  125. package/dist/pull-requests/github-publisher.mjs.map +1 -1
  126. package/dist/pull-requests.d.mts +1 -0
  127. package/dist/sandbox/index.d.mts +1 -0
  128. package/dist/schema/agent-route.d.mts +19 -1
  129. package/dist/schema/agent-route.mjs +19 -1
  130. package/dist/schema/agent-route.mjs.map +1 -1
  131. package/dist/schema/factory-config.d.mts +27 -0
  132. package/dist/schema/factory-config.mjs +33 -3
  133. package/dist/schema/factory-config.mjs.map +1 -1
  134. package/dist/schema/repository.d.mts +4 -0
  135. package/dist/schema/repository.mjs +5 -1
  136. package/dist/schema/repository.mjs.map +1 -1
  137. package/dist/schema/session.d.mts +1 -0
  138. package/dist/schema/session.mjs +1 -0
  139. package/dist/schema/session.mjs.map +1 -1
  140. package/dist/schema/slack-pr-notifications.d.mts +12 -0
  141. package/dist/schema/slack-pr-notifications.mjs +11 -0
  142. package/dist/schema/slack-pr-notifications.mjs.map +1 -0
  143. package/dist/schema/task-graph.d.mts +39 -0
  144. package/dist/schema/task.d.mts +1 -0
  145. package/dist/schema/task.mjs +2 -1
  146. package/dist/schema/task.mjs.map +1 -1
  147. package/dist/schema/transcript.d.mts +6 -0
  148. package/dist/schema/transcript.mjs +2 -1
  149. package/dist/schema/transcript.mjs.map +1 -1
  150. package/dist/schema/work.d.mts +52 -3
  151. package/dist/schema/work.mjs.map +1 -1
  152. package/dist/session-previews.d.mts +76 -0
  153. package/dist/session-previews.mjs +55 -0
  154. package/dist/session-previews.mjs.map +1 -0
  155. package/dist/session-review.d.mts +120 -0
  156. package/dist/session-review.mjs +79 -0
  157. package/dist/session-review.mjs.map +1 -0
  158. package/dist/signal-triage.mjs +1 -1
  159. package/dist/signals.d.mts +1 -0
  160. package/dist/stall.d.mts +4 -1
  161. package/dist/stall.mjs +6 -2
  162. package/dist/stall.mjs.map +1 -1
  163. package/dist/store/driver.d.mts +1 -1
  164. package/dist/store/driver.mjs.map +1 -1
  165. package/dist/store/engine.d.mts +206 -8
  166. package/dist/store/engine.mjs +147 -13
  167. package/dist/store/engine.mjs.map +1 -1
  168. package/dist/store/memory.d.mts +18 -1
  169. package/dist/store/memory.mjs +18 -1
  170. package/dist/store/memory.mjs.map +1 -1
  171. package/dist/store/slack-pr-notifications.d.mts +44 -0
  172. package/dist/store/slack-pr-notifications.mjs +121 -0
  173. package/dist/store/slack-pr-notifications.mjs.map +1 -0
  174. package/dist/store/task-work.d.mts +121 -6
  175. package/dist/store/task-work.mjs +7 -4
  176. package/dist/store/task-work.mjs.map +1 -1
  177. package/dist/sweep.d.mts +28 -6
  178. package/dist/sweep.mjs +34 -6
  179. package/dist/sweep.mjs.map +1 -1
  180. package/dist/task-graph-view.d.mts +3 -0
  181. package/dist/tasks.d.mts +3 -3
  182. package/dist/tasks.mjs +3 -3
  183. package/dist/vercel-git.d.mts +35 -3
  184. package/dist/vercel-git.mjs +265 -33
  185. package/dist/vercel-git.mjs.map +1 -1
  186. package/dist/vercel-github-api.d.mts +103 -0
  187. package/dist/vercel-github-api.mjs +363 -0
  188. package/dist/vercel-github-api.mjs.map +1 -0
  189. package/dist/vercel.d.mts +3 -2
  190. package/dist/vercel.mjs +3 -2
  191. package/dist/vercel.mjs.map +1 -1
  192. package/dist/work-triage.d.mts +1 -0
  193. package/dist/workflows.d.mts +102 -4
  194. package/dist/workflows.mjs +55 -2
  195. package/dist/workflows.mjs.map +1 -1
  196. package/dist/workspace-files-git.d.mts +15 -0
  197. package/dist/workspace-files-git.mjs +61 -0
  198. package/dist/workspace-files-git.mjs.map +1 -0
  199. package/dist/workspace-files.d.mts +107 -0
  200. package/dist/workspace-files.mjs +74 -0
  201. package/dist/workspace-files.mjs.map +1 -0
  202. package/docs/getting-started.md +104 -0
  203. package/docs/index.md +100 -0
  204. package/docs/recipes/cancellation.md +215 -0
  205. package/docs/recipes/custom-workflow.md +153 -0
  206. package/docs/recipes/dependent-tasks.md +207 -0
  207. package/docs/recipes/eve-agent.md +277 -0
  208. package/docs/recipes/human-input.md +204 -0
  209. package/docs/recipes/persistence-recovery.md +268 -0
  210. package/docs/recipes/retry-recovery.md +241 -0
  211. package/docs/recipes/task-messaging.md +215 -0
  212. package/docs/recipes/typed-eve-result.md +161 -0
  213. package/docs/runtime-integration.md +137 -0
  214. package/package.json +17 -6
@@ -1,6 +1,61 @@
1
1
  import { factoryTaskAttemptAttribute, factoryTaskAttribute, taskBindingForSession } from "./task-session.mjs";
2
2
  //#region src/eve/task-execution.ts
3
+ /**
4
+ * Requires the authenticated session's current running Eve Task execution and attempt.
5
+ *
6
+ * @remarks
7
+ * `requestedTaskId` is a consistency check, not an authority source. This function reads the Task
8
+ * and attempt established by `withFactoryTask`, rejects conflicting current-auth attributes, and
9
+ * verifies that durable state still points to the same Eve session and attempt. Use it immediately
10
+ * before a mutating tool applies effects or completes workflow output. A cancellation, retry, or
11
+ * replacement session fences the stale caller out.
12
+ *
13
+ * The check does not complete the Task and does not make a following external side effect atomic
14
+ * with Factory state. Pass the returned attempt and execution as the completion fence when writing
15
+ * output.
16
+ *
17
+ * @param stores - Task store used to reread canonical execution state.
18
+ * @param ctx - Eve tool or hook context containing initiator and current authentication.
19
+ * @param requestedTaskId - Task ID from the tool input, required to match the authenticated binding.
20
+ * @returns The currently running Task bound to this exact Eve session and attempt.
21
+ * @throws When authentication is missing or malformed, the requested Task differs, or durable execution was replaced or stopped.
22
+ * @see The shipped `docs/recipes/typed-eve-result.md` recipe for a complete typed tool.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { requireTaskExecution } from "@vercel/factory";
27
+ * import type { FactoryStores } from "@vercel/factory/storage";
28
+ * import type { TaskId } from "@vercel/factory/tasks";
29
+ * import { defineWorkflow } from "@vercel/factory/workflows";
30
+ * import type { ToolContext } from "eve/tools";
31
+ * import { z } from "zod";
32
+ *
33
+ * declare const stores: FactoryStores;
34
+ * declare const ctx: ToolContext;
35
+ * declare const taskId: TaskId;
36
+ * const workflow = defineWorkflow({
37
+ * id: "greeting",
38
+ * version: 1,
39
+ * input: z.object({ name: z.string() }),
40
+ * output: z.object({ message: z.string() }),
41
+ * });
42
+ *
43
+ * const task = await requireTaskExecution(stores, ctx, taskId);
44
+ * if (task.execution?.provider !== "eve") throw new Error("Expected Eve execution");
45
+ * await stores.work.completeWorkflow({
46
+ * task: { taskId: task.id, attempt: task.attempt },
47
+ * workflow: workflow.binding,
48
+ * output: { message: "Hello" },
49
+ * fence: { from: "running", execution: task.execution },
50
+ * });
51
+ * ```
52
+ */
3
53
  async function requireTaskExecution(stores, ctx, requestedTaskId) {
54
+ const task = await requireTaskExecutionIdentity(stores, ctx, requestedTaskId);
55
+ if (task.state !== "running") throw new Error("Task execution is missing, inactive, or replaced");
56
+ return task;
57
+ }
58
+ async function requireTaskExecutionIdentity(stores, ctx, requestedTaskId) {
4
59
  const auth = ctx.session?.auth;
5
60
  const binding = taskBindingForSession(auth?.initiator);
6
61
  if (binding.taskId !== requestedTaskId) throw new Error("Task does not match the authenticated session");
@@ -9,10 +64,10 @@ async function requireTaskExecution(stores, ctx, requestedTaskId) {
9
64
  if (current !== void 0 && current !== auth?.initiator?.attributes[key]) throw new Error("Task binding does not match the current authentication");
10
65
  }
11
66
  const task = await stores.tasks.get(binding.taskId);
12
- if (task?.state !== "running" || task.attempt !== binding.attempt || task.execution?.provider !== "eve" || task.execution.sessionId !== ctx.session.id) throw new Error("Task execution is missing, inactive, or replaced");
67
+ if (task === null || task.attempt !== binding.attempt || task.execution?.provider !== "eve" || task.execution.sessionId !== ctx.session.id) throw new Error("Task execution is missing, inactive, or replaced");
13
68
  return task;
14
69
  }
15
70
  //#endregion
16
- export { requireTaskExecution };
71
+ export { requireTaskExecution, requireTaskExecutionIdentity };
17
72
 
18
73
  //# sourceMappingURL=task-execution.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-execution.mjs","names":[],"sources":["../../src/eve/task-execution.ts"],"sourcesContent":["import type { SessionContext } from \"eve/context\";\nimport type { TaskId } from \"../schema/id\";\nimport type { FactoryStores } from \"../store/engine\";\nimport {\n factoryTaskAttribute,\n factoryTaskAttemptAttribute,\n taskBindingForSession,\n} from \"./task-session\";\n\nexport async function requireTaskExecution(\n stores: Pick<FactoryStores, \"tasks\">,\n ctx: Pick<SessionContext, \"session\">,\n requestedTaskId: TaskId,\n) {\n const auth = ctx.session?.auth;\n const binding = taskBindingForSession(auth?.initiator);\n if (binding.taskId !== requestedTaskId) {\n throw new Error(\"Task does not match the authenticated session\");\n }\n for (const key of [factoryTaskAttribute, factoryTaskAttemptAttribute]) {\n const current = auth?.current?.attributes[key];\n if (current !== undefined && current !== auth?.initiator?.attributes[key]) {\n throw new Error(\"Task binding does not match the current authentication\");\n }\n }\n const task = await stores.tasks.get(binding.taskId);\n if (\n task?.state !== \"running\" ||\n task.attempt !== binding.attempt ||\n task.execution?.provider !== \"eve\" ||\n task.execution.sessionId !== ctx.session.id\n ) {\n throw new Error(\"Task execution is missing, inactive, or replaced\");\n }\n return task;\n}\n"],"mappings":";;AASA,eAAsB,qBACpB,QACA,KACA,iBACA;CACA,MAAM,OAAO,IAAI,SAAS;CAC1B,MAAM,UAAU,sBAAsB,MAAM,SAAS;CACrD,IAAI,QAAQ,WAAW,iBACrB,MAAM,IAAI,MAAM,+CAA+C;CAEjE,KAAK,MAAM,OAAO,CAAC,sBAAsB,2BAA2B,GAAG;EACrE,MAAM,UAAU,MAAM,SAAS,WAAW;EAC1C,IAAI,YAAY,KAAA,KAAa,YAAY,MAAM,WAAW,WAAW,MACnE,MAAM,IAAI,MAAM,wDAAwD;CAE5E;CACA,MAAM,OAAO,MAAM,OAAO,MAAM,IAAI,QAAQ,MAAM;CAClD,IACE,MAAM,UAAU,aAChB,KAAK,YAAY,QAAQ,WACzB,KAAK,WAAW,aAAa,SAC7B,KAAK,UAAU,cAAc,IAAI,QAAQ,IAEzC,MAAM,IAAI,MAAM,kDAAkD;CAEpE,OAAO;AACT"}
1
+ {"version":3,"file":"task-execution.mjs","names":[],"sources":["../../src/eve/task-execution.ts"],"sourcesContent":["import type { SessionContext } from \"eve/context\";\nimport type { TaskId } from \"../schema/id\";\nimport type { FactoryStores } from \"../store/engine\";\nimport {\n factoryTaskAttribute,\n factoryTaskAttemptAttribute,\n taskBindingForSession,\n} from \"./task-session\";\n\n/**\n * Requires the authenticated session's current running Eve Task execution and attempt.\n *\n * @remarks\n * `requestedTaskId` is a consistency check, not an authority source. This function reads the Task\n * and attempt established by `withFactoryTask`, rejects conflicting current-auth attributes, and\n * verifies that durable state still points to the same Eve session and attempt. Use it immediately\n * before a mutating tool applies effects or completes workflow output. A cancellation, retry, or\n * replacement session fences the stale caller out.\n *\n * The check does not complete the Task and does not make a following external side effect atomic\n * with Factory state. Pass the returned attempt and execution as the completion fence when writing\n * output.\n *\n * @param stores - Task store used to reread canonical execution state.\n * @param ctx - Eve tool or hook context containing initiator and current authentication.\n * @param requestedTaskId - Task ID from the tool input, required to match the authenticated binding.\n * @returns The currently running Task bound to this exact Eve session and attempt.\n * @throws When authentication is missing or malformed, the requested Task differs, or durable execution was replaced or stopped.\n * @see The shipped `docs/recipes/typed-eve-result.md` recipe for a complete typed tool.\n *\n * @example\n * ```ts\n * import { requireTaskExecution } from \"@vercel/factory\";\n * import type { FactoryStores } from \"@vercel/factory/storage\";\n * import type { TaskId } from \"@vercel/factory/tasks\";\n * import { defineWorkflow } from \"@vercel/factory/workflows\";\n * import type { ToolContext } from \"eve/tools\";\n * import { z } from \"zod\";\n *\n * declare const stores: FactoryStores;\n * declare const ctx: ToolContext;\n * declare const taskId: TaskId;\n * const workflow = defineWorkflow({\n * id: \"greeting\",\n * version: 1,\n * input: z.object({ name: z.string() }),\n * output: z.object({ message: z.string() }),\n * });\n *\n * const task = await requireTaskExecution(stores, ctx, taskId);\n * if (task.execution?.provider !== \"eve\") throw new Error(\"Expected Eve execution\");\n * await stores.work.completeWorkflow({\n * task: { taskId: task.id, attempt: task.attempt },\n * workflow: workflow.binding,\n * output: { message: \"Hello\" },\n * fence: { from: \"running\", execution: task.execution },\n * });\n * ```\n */\nexport async function requireTaskExecution(\n stores: Pick<FactoryStores, \"tasks\">,\n ctx: { session: Pick<SessionContext[\"session\"], \"id\" | \"auth\"> },\n requestedTaskId: TaskId,\n) {\n const task = await requireTaskExecutionIdentity(stores, ctx, requestedTaskId);\n if (task.state !== \"running\") {\n throw new Error(\"Task execution is missing, inactive, or replaced\");\n }\n return task;\n}\n\n// Completion replay also checks identity; its caller must restrict lifecycle states and effects.\nexport async function requireTaskExecutionIdentity(\n stores: Pick<FactoryStores, \"tasks\">,\n ctx: { session: Pick<SessionContext[\"session\"], \"id\" | \"auth\"> },\n requestedTaskId: TaskId,\n) {\n const auth = ctx.session?.auth;\n const binding = taskBindingForSession(auth?.initiator);\n if (binding.taskId !== requestedTaskId) {\n throw new Error(\"Task does not match the authenticated session\");\n }\n for (const key of [factoryTaskAttribute, factoryTaskAttemptAttribute]) {\n const current = auth?.current?.attributes[key];\n if (current !== undefined && current !== auth?.initiator?.attributes[key]) {\n throw new Error(\"Task binding does not match the current authentication\");\n }\n }\n const task = await stores.tasks.get(binding.taskId);\n if (\n task === null ||\n task.attempt !== binding.attempt ||\n task.execution?.provider !== \"eve\" ||\n task.execution.sessionId !== ctx.session.id\n ) {\n throw new Error(\"Task execution is missing, inactive, or replaced\");\n }\n return task;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,eAAsB,qBACpB,QACA,KACA,iBACA;CACA,MAAM,OAAO,MAAM,6BAA6B,QAAQ,KAAK,eAAe;CAC5E,IAAI,KAAK,UAAU,WACjB,MAAM,IAAI,MAAM,kDAAkD;CAEpE,OAAO;AACT;AAGA,eAAsB,6BACpB,QACA,KACA,iBACA;CACA,MAAM,OAAO,IAAI,SAAS;CAC1B,MAAM,UAAU,sBAAsB,MAAM,SAAS;CACrD,IAAI,QAAQ,WAAW,iBACrB,MAAM,IAAI,MAAM,+CAA+C;CAEjE,KAAK,MAAM,OAAO,CAAC,sBAAsB,2BAA2B,GAAG;EACrE,MAAM,UAAU,MAAM,SAAS,WAAW;EAC1C,IAAI,YAAY,KAAA,KAAa,YAAY,MAAM,WAAW,WAAW,MACnE,MAAM,IAAI,MAAM,wDAAwD;CAE5E;CACA,MAAM,OAAO,MAAM,OAAO,MAAM,IAAI,QAAQ,MAAM;CAClD,IACE,SAAS,QACT,KAAK,YAAY,QAAQ,WACzB,KAAK,WAAW,aAAa,SAC7B,KAAK,UAAU,cAAc,IAAI,QAAQ,IAEzC,MAAM,IAAI,MAAM,kDAAkD;CAEpE,OAAO;AACT"}
@@ -33,7 +33,25 @@ interface FactoryConversationSessionBinding {
33
33
  }
34
34
  /** Test whether authenticated Eve session state declares a Factory Task binding. */
35
35
  declare function hasFactoryTaskBinding(auth: SessionInitiator): boolean;
36
- /** Encode a Task ID and attempt fence as headers for an Eve session request. */
36
+ /**
37
+ * Encodes a Task ID and attempt fence as headers for a trusted Eve session request.
38
+ *
39
+ * @remarks
40
+ * These headers are a transport envelope, not standalone authorization. The receiving Eve route
41
+ * must authenticate the trusted caller and wrap that authentication with `withFactoryTask`.
42
+ *
43
+ * @param task - Exact Task and attempt claimed by dispatch.
44
+ * @returns Headers consumed by `withFactoryTask` on the target Eve channel.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { taskHeaders } from "@vercel/factory";
49
+ * import type { TaskAttemptRef } from "@vercel/factory/execution";
50
+ *
51
+ * declare const task: TaskAttemptRef;
52
+ * const headers = taskHeaders(task);
53
+ * ```
54
+ */
37
55
  declare function taskHeaders(task: TaskAttemptRef): Record<string, string>;
38
56
  /** Encode a conversation's repository scope and reply destination as Eve request headers. */
39
57
  declare function conversationHeaders(input: {
@@ -42,7 +60,31 @@ declare function conversationHeaders(input: {
42
60
  }): Record<string, string>;
43
61
  /** Add a validated Task ID and attempt fence to an authenticated Eve initiator. */
44
62
  declare function taskSessionAuth(auth: AuthenticatedSessionInitiator, task: TaskAttemptRef): AuthenticatedSessionInitiator;
45
- /** Require valid Factory Task headers and bind them into the authenticated Eve initiator. */
63
+ /**
64
+ * Requires valid Factory Task headers and binds them into an authenticated Eve initiator.
65
+ *
66
+ * @remarks
67
+ * The wrapped authenticator runs first. If it returns no identity, that result is preserved. For
68
+ * an authenticated caller, both Factory headers are mandatory and validated before their Task ID
69
+ * and attempt are copied into session attributes. This records claimed identity; mutating tools
70
+ * call `requireTaskExecution` to recheck it against current durable Task state.
71
+ *
72
+ * Mount this wrapper only on routes reached through a trusted launcher that supplies
73
+ * `taskHeaders`. It does not independently verify that the caller may claim an arbitrary Task.
74
+ *
75
+ * @param authenticate - Existing Eve request authenticator for the trusted launcher.
76
+ * @returns An authenticator that adds the validated Factory Task binding.
77
+ * @throws `ForbiddenError` when either Task header is absent or malformed.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * import { withFactoryTask } from "@vercel/factory";
82
+ * import { localDev } from "eve/channels/auth";
83
+ * import { eveChannel } from "eve/channels/eve";
84
+ *
85
+ * export default eveChannel({ auth: [withFactoryTask(localDev())] });
86
+ * ```
87
+ */
46
88
  declare function withFactoryTask(authenticate: AuthFn<Request>): AuthFn<Request>;
47
89
  /** Bind optional Task and conversation headers while rejecting partial or malformed context. */
48
90
  declare function withOptionalFactoryTask(authenticate: AuthFn<Request>): AuthFn<Request>;
@@ -27,7 +27,25 @@ const factoryReplyAddressAttribute = "factoryReplyAddress";
27
27
  function hasFactoryTaskBinding(auth) {
28
28
  return auth?.attributes[factoryTaskAttribute] !== void 0;
29
29
  }
30
- /** Encode a Task ID and attempt fence as headers for an Eve session request. */
30
+ /**
31
+ * Encodes a Task ID and attempt fence as headers for a trusted Eve session request.
32
+ *
33
+ * @remarks
34
+ * These headers are a transport envelope, not standalone authorization. The receiving Eve route
35
+ * must authenticate the trusted caller and wrap that authentication with `withFactoryTask`.
36
+ *
37
+ * @param task - Exact Task and attempt claimed by dispatch.
38
+ * @returns Headers consumed by `withFactoryTask` on the target Eve channel.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { taskHeaders } from "@vercel/factory";
43
+ * import type { TaskAttemptRef } from "@vercel/factory/execution";
44
+ *
45
+ * declare const task: TaskAttemptRef;
46
+ * const headers = taskHeaders(task);
47
+ * ```
48
+ */
31
49
  function taskHeaders(task) {
32
50
  return {
33
51
  [factoryTaskHeader]: task.taskId,
@@ -53,7 +71,31 @@ function taskSessionAuth(auth, task) {
53
71
  }
54
72
  };
55
73
  }
56
- /** Require valid Factory Task headers and bind them into the authenticated Eve initiator. */
74
+ /**
75
+ * Requires valid Factory Task headers and binds them into an authenticated Eve initiator.
76
+ *
77
+ * @remarks
78
+ * The wrapped authenticator runs first. If it returns no identity, that result is preserved. For
79
+ * an authenticated caller, both Factory headers are mandatory and validated before their Task ID
80
+ * and attempt are copied into session attributes. This records claimed identity; mutating tools
81
+ * call `requireTaskExecution` to recheck it against current durable Task state.
82
+ *
83
+ * Mount this wrapper only on routes reached through a trusted launcher that supplies
84
+ * `taskHeaders`. It does not independently verify that the caller may claim an arbitrary Task.
85
+ *
86
+ * @param authenticate - Existing Eve request authenticator for the trusted launcher.
87
+ * @returns An authenticator that adds the validated Factory Task binding.
88
+ * @throws `ForbiddenError` when either Task header is absent or malformed.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * import { withFactoryTask } from "@vercel/factory";
93
+ * import { localDev } from "eve/channels/auth";
94
+ * import { eveChannel } from "eve/channels/eve";
95
+ *
96
+ * export default eveChannel({ auth: [withFactoryTask(localDev())] });
97
+ * ```
98
+ */
57
99
  function withFactoryTask(authenticate) {
58
100
  return async (request) => {
59
101
  const auth = await authenticate(request);
@@ -1 +1 @@
1
- {"version":3,"file":"task-session.mjs","names":[],"sources":["../../src/eve/task-session.ts"],"sourcesContent":["import { repositoryIdSchema, taskIdSchema } from \"../schema/id\";\nimport { replyToSchema, type ReplyTo } from \"../schema/common\";\nimport { taskAttemptRef, type TaskAttemptRef } from \"../schema/execution\";\nimport type { RepositoryId, TaskId } from \"../schema/id\";\nimport { ForbiddenError, type AuthFn } from \"eve/channels/auth\";\nimport type { SessionContext } from \"eve/context\";\n\n/** HTTP header that carries the authenticated Factory Task ID into an Eve session. */\nexport const factoryTaskHeader = \"x-factory-task-id\";\n/** Eve authentication attribute that stores the bound Factory Task ID. */\nexport const factoryTaskAttribute = \"factoryTaskId\";\n/** HTTP header that carries the Task attempt fence into an Eve session. */\nexport const factoryTaskAttemptHeader = \"x-factory-task-attempt\";\n/** Eve authentication attribute that stores the bound Task attempt fence. */\nexport const factoryTaskAttemptAttribute = \"factoryTaskAttempt\";\n/** HTTP header that carries the authenticated repository authorization envelope. */\nexport const factoryRepositoryIdsHeader = \"x-factory-repository-ids\";\n/** HTTP header that carries the authenticated conversation reply channel. */\nexport const factoryReplyChannelHeader = \"x-factory-reply-channel\";\n/** HTTP header that carries the authenticated conversation reply address. */\nexport const factoryReplyAddressHeader = \"x-factory-reply-address\";\n/** Eve authentication attribute that stores the authorized repository IDs. */\nexport const factoryRepositoryIdsAttribute = \"factoryRepositoryIds\";\n/** Eve authentication attribute that stores the conversation reply channel. */\nexport const factoryReplyChannelAttribute = \"factoryReplyChannel\";\n/** Eve authentication attribute that stores the conversation reply address. */\nexport const factoryReplyAddressAttribute = \"factoryReplyAddress\";\n\ntype SessionInitiator = SessionContext[\"session\"][\"auth\"][\"initiator\"];\ntype AuthenticatedSessionInitiator = NonNullable<SessionInitiator>;\n\n/** The repository authorization envelope and reply destination bound to a conversation. */\nexport interface FactoryConversationSessionBinding {\n repositoryIds: readonly RepositoryId[];\n replyTo: ReplyTo;\n}\n\n/** Test whether authenticated Eve session state declares a Factory Task binding. */\nexport function hasFactoryTaskBinding(auth: SessionInitiator): boolean {\n return auth?.attributes[factoryTaskAttribute] !== undefined;\n}\n\n/** Encode a Task ID and attempt fence as headers for an Eve session request. */\nexport function taskHeaders(task: TaskAttemptRef): Record<string, string> {\n return {\n [factoryTaskHeader]: task.taskId,\n [factoryTaskAttemptHeader]: String(task.attempt),\n };\n}\n\n/** Encode a conversation's repository scope and reply destination as Eve request headers. */\nexport function conversationHeaders(input: {\n repositoryIds: readonly RepositoryId[];\n replyTo: ReplyTo;\n}): Record<string, string> {\n return {\n [factoryRepositoryIdsHeader]: input.repositoryIds.join(\",\"),\n [factoryReplyChannelHeader]: input.replyTo.channel,\n [factoryReplyAddressHeader]: input.replyTo.address,\n };\n}\n\n/** Add a validated Task ID and attempt fence to an authenticated Eve initiator. */\nexport function taskSessionAuth(\n auth: AuthenticatedSessionInitiator,\n task: TaskAttemptRef,\n): AuthenticatedSessionInitiator {\n return {\n ...auth,\n attributes: {\n ...auth.attributes,\n [factoryTaskAttribute]: task.taskId,\n [factoryTaskAttemptAttribute]: String(task.attempt),\n },\n };\n}\n\n/** Require valid Factory Task headers and bind them into the authenticated Eve initiator. */\nexport function withFactoryTask(authenticate: AuthFn<Request>): AuthFn<Request> {\n return async (request) => {\n const auth = await authenticate(request);\n if (auth == null) return auth;\n const parsedTask = taskIdSchema.safeParse(request.headers.get(factoryTaskHeader));\n const parsedAttempt = parseTaskAttempt(request.headers.get(factoryTaskAttemptHeader));\n if (!parsedTask.success || parsedAttempt === null) throw new ForbiddenError();\n return taskSessionAuth(auth, taskAttemptRef(parsedTask.data, parsedAttempt));\n };\n}\n\n/** Bind optional Task and conversation headers while rejecting partial or malformed context. */\nexport function withOptionalFactoryTask(authenticate: AuthFn<Request>): AuthFn<Request> {\n return async (request) => {\n const auth = await authenticate(request);\n if (auth == null) return auth;\n const taskId = request.headers.get(factoryTaskHeader);\n const taskAttempt = request.headers.get(factoryTaskAttemptHeader);\n const repositoryIds = request.headers.get(factoryRepositoryIdsHeader);\n const replyChannel = request.headers.get(factoryReplyChannelHeader);\n const replyAddress = request.headers.get(factoryReplyAddressHeader);\n const hasConversationBinding =\n repositoryIds !== null || replyChannel !== null || replyAddress !== null;\n if (taskId === null && taskAttempt === null && !hasConversationBinding) return auth;\n const parsedTask = taskId === null ? null : taskIdSchema.safeParse(taskId);\n const parsedAttempt = parseTaskAttempt(taskAttempt);\n const parsedRepositories =\n repositoryIds === null\n ? null\n : repositoryIdSchema.array().min(1).safeParse(repositoryIds.split(\",\").filter(Boolean));\n const parsedReplyTo =\n replyChannel === null || replyAddress === null\n ? null\n : replyToSchema.safeParse({ channel: replyChannel, address: replyAddress });\n if (\n (parsedTask !== null && !parsedTask.success) ||\n (parsedTask === null) !== (parsedAttempt === null) ||\n (hasConversationBinding &&\n (parsedRepositories === null ||\n !parsedRepositories.success ||\n parsedReplyTo === null ||\n !parsedReplyTo.success))\n ) {\n throw new ForbiddenError();\n }\n const conversationAttributes: Record<string, string> =\n parsedRepositories?.success === true && parsedReplyTo?.success === true\n ? {\n [factoryRepositoryIdsAttribute]: parsedRepositories.data.join(\",\"),\n [factoryReplyChannelAttribute]: parsedReplyTo.data.channel,\n [factoryReplyAddressAttribute]: parsedReplyTo.data.address,\n }\n : {};\n return {\n ...auth,\n attributes: {\n ...auth.attributes,\n ...(parsedTask === null || parsedAttempt === null\n ? {}\n : {\n [factoryTaskAttribute]: parsedTask.data,\n [factoryTaskAttemptAttribute]: String(parsedAttempt),\n }),\n ...conversationAttributes,\n },\n };\n };\n}\n\n/** Read the required Factory Task ID from authenticated Eve session state. */\nexport function requireTaskIdForSession(auth: SessionInitiator): TaskId {\n return taskIdSchema.parse(auth?.attributes[factoryTaskAttribute]);\n}\n\n/** Read an optional Task ID from authenticated session state, rejecting malformed values. */\nexport function optionalTaskIdForSession(auth: SessionInitiator): TaskId | null {\n const value = auth?.attributes[factoryTaskAttribute];\n return value === undefined ? null : taskIdSchema.parse(value);\n}\n\n/** Read the complete, attempt-fenced Task binding established by Factory authentication. */\nexport function taskBindingForSession(auth: SessionInitiator): TaskAttemptRef {\n const taskId = requireTaskIdForSession(auth);\n const rawAttempt = auth?.attributes[factoryTaskAttemptAttribute];\n const attempt = parseTaskAttempt(typeof rawAttempt === \"string\" ? rawAttempt : null);\n if (attempt === null) throw new Error(\"Factory session Task attempt binding is invalid.\");\n return { taskId, attempt };\n}\n\n/** Read the repository authorization envelope, or null when the session has none. */\nexport function repositoryIdsForSession(auth: SessionInitiator): readonly RepositoryId[] | null {\n const value = auth?.attributes[factoryRepositoryIdsAttribute];\n if (value === undefined) return null;\n return repositoryIdSchema\n .array()\n .min(1)\n .parse(typeof value === \"string\" ? value.split(\",\").filter(Boolean) : value);\n}\n\n/** Read the authenticated reply destination, or null when the session has none. */\nexport function replyToForSession(auth: SessionInitiator): ReplyTo | null {\n const channel = auth?.attributes[factoryReplyChannelAttribute];\n const address = auth?.attributes[factoryReplyAddressAttribute];\n if (channel === undefined && address === undefined) return null;\n return replyToSchema.parse({ channel, address });\n}\n\n/** Read the complete conversation binding, rejecting partial authentication state. */\nexport function conversationBindingForSession(\n auth: SessionInitiator,\n): FactoryConversationSessionBinding | null {\n const repositoryIds = repositoryIdsForSession(auth);\n const replyTo = replyToForSession(auth);\n if (repositoryIds === null && replyTo === null) return null;\n if (repositoryIds === null || replyTo === null) {\n throw new Error(\"Factory session conversation binding is incomplete.\");\n }\n return { repositoryIds, replyTo };\n}\n\n/** Authenticated conversation authority assembled from both initiator and current session state. */\nexport interface ConversationBinding {\n replyTo: ReplyTo;\n /**\n * Repository envelopes the binding carries: the initiator's, plus the current\n * auth's when it restates one. Admitted work must sit inside every envelope.\n */\n repositoryEnvelopes: readonly (readonly RepositoryId[])[];\n parentTask?: TaskAttemptRef;\n}\n\nconst conversationBindingAttributes = [\n factoryReplyChannelAttribute,\n factoryReplyAddressAttribute,\n factoryTaskAttribute,\n factoryTaskAttemptAttribute,\n] as const;\n\n/**\n * Read and validate the conversation binding carried on a session's auth pair.\n * The initiator's attributes are authoritative; any binding attribute the\n * current auth restates must agree with them. This is the only reader of the\n * binding attribute encoding; callers never touch `attributes` themselves.\n *\n * Distinct from `conversationBindingForSession`, which reads the initiator\n * alone: admission needs every envelope the pair carries, so that work cannot\n * be admitted into a repository only one side of the pair authorizes.\n */\nexport function conversationBinding(auth: {\n initiator: AuthenticatedSessionInitiator;\n current: AuthenticatedSessionInitiator;\n}): ConversationBinding {\n const attributes = auth.initiator.attributes;\n const replyTo = replyToSchema.parse({\n channel: attributes[factoryReplyChannelAttribute],\n address: attributes[factoryReplyAddressAttribute],\n });\n for (const key of conversationBindingAttributes) {\n if (\n auth.current.attributes[key] !== undefined &&\n auth.current.attributes[key] !== attributes[key]\n )\n throw new Error(\"Conversation binding does not match the authenticated conversation\");\n }\n const repositoryEnvelopes: (readonly RepositoryId[])[] = [];\n for (const [index, entry] of [auth.initiator, auth.current].entries()) {\n const raw = entry.attributes[factoryRepositoryIdsAttribute];\n if (index === 1 && raw === undefined) continue;\n repositoryEnvelopes.push(\n repositoryIdSchema\n .array()\n .min(1)\n .parse(typeof raw === \"string\" ? raw.split(\",\") : raw),\n );\n }\n let parentTask: ConversationBinding[\"parentTask\"];\n if (\n attributes[factoryTaskAttribute] !== undefined ||\n attributes[factoryTaskAttemptAttribute] !== undefined\n ) {\n const taskId = taskIdSchema.parse(attributes[factoryTaskAttribute]);\n const rawAttempt = attributes[factoryTaskAttemptAttribute];\n const attempt = typeof rawAttempt === \"string\" ? parseTaskAttempt(rawAttempt) : null;\n if (attempt === null) throw new Error(\"Conversation binding carries an invalid parent attempt\");\n parentTask = taskAttemptRef(taskId, attempt);\n }\n return { replyTo, repositoryEnvelopes, parentTask };\n}\n\nfunction parseTaskAttempt(value: string | null): number | null {\n if (value === null || !/^[1-9]\\d*$/u.test(value)) return null;\n const attempt = Number(value);\n return Number.isSafeInteger(attempt) ? attempt : null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"task-session.mjs","names":[],"sources":["../../src/eve/task-session.ts"],"sourcesContent":["import { repositoryIdSchema, taskIdSchema } from \"../schema/id\";\nimport { replyToSchema, type ReplyTo } from \"../schema/common\";\nimport { taskAttemptRef, type TaskAttemptRef } from \"../schema/execution\";\nimport type { RepositoryId, TaskId } from \"../schema/id\";\nimport { ForbiddenError, type AuthFn } from \"eve/channels/auth\";\nimport type { SessionContext } from \"eve/context\";\n\n/** HTTP header that carries the authenticated Factory Task ID into an Eve session. */\nexport const factoryTaskHeader = \"x-factory-task-id\";\n/** Eve authentication attribute that stores the bound Factory Task ID. */\nexport const factoryTaskAttribute = \"factoryTaskId\";\n/** HTTP header that carries the Task attempt fence into an Eve session. */\nexport const factoryTaskAttemptHeader = \"x-factory-task-attempt\";\n/** Eve authentication attribute that stores the bound Task attempt fence. */\nexport const factoryTaskAttemptAttribute = \"factoryTaskAttempt\";\n/** HTTP header that carries the authenticated repository authorization envelope. */\nexport const factoryRepositoryIdsHeader = \"x-factory-repository-ids\";\n/** HTTP header that carries the authenticated conversation reply channel. */\nexport const factoryReplyChannelHeader = \"x-factory-reply-channel\";\n/** HTTP header that carries the authenticated conversation reply address. */\nexport const factoryReplyAddressHeader = \"x-factory-reply-address\";\n/** Eve authentication attribute that stores the authorized repository IDs. */\nexport const factoryRepositoryIdsAttribute = \"factoryRepositoryIds\";\n/** Eve authentication attribute that stores the conversation reply channel. */\nexport const factoryReplyChannelAttribute = \"factoryReplyChannel\";\n/** Eve authentication attribute that stores the conversation reply address. */\nexport const factoryReplyAddressAttribute = \"factoryReplyAddress\";\n\ntype SessionInitiator = SessionContext[\"session\"][\"auth\"][\"initiator\"];\ntype AuthenticatedSessionInitiator = NonNullable<SessionInitiator>;\n\n/** The repository authorization envelope and reply destination bound to a conversation. */\nexport interface FactoryConversationSessionBinding {\n repositoryIds: readonly RepositoryId[];\n replyTo: ReplyTo;\n}\n\n/** Test whether authenticated Eve session state declares a Factory Task binding. */\nexport function hasFactoryTaskBinding(auth: SessionInitiator): boolean {\n return auth?.attributes[factoryTaskAttribute] !== undefined;\n}\n\n/**\n * Encodes a Task ID and attempt fence as headers for a trusted Eve session request.\n *\n * @remarks\n * These headers are a transport envelope, not standalone authorization. The receiving Eve route\n * must authenticate the trusted caller and wrap that authentication with `withFactoryTask`.\n *\n * @param task - Exact Task and attempt claimed by dispatch.\n * @returns Headers consumed by `withFactoryTask` on the target Eve channel.\n *\n * @example\n * ```ts\n * import { taskHeaders } from \"@vercel/factory\";\n * import type { TaskAttemptRef } from \"@vercel/factory/execution\";\n *\n * declare const task: TaskAttemptRef;\n * const headers = taskHeaders(task);\n * ```\n */\nexport function taskHeaders(task: TaskAttemptRef): Record<string, string> {\n return {\n [factoryTaskHeader]: task.taskId,\n [factoryTaskAttemptHeader]: String(task.attempt),\n };\n}\n\n/** Encode a conversation's repository scope and reply destination as Eve request headers. */\nexport function conversationHeaders(input: {\n repositoryIds: readonly RepositoryId[];\n replyTo: ReplyTo;\n}): Record<string, string> {\n return {\n [factoryRepositoryIdsHeader]: input.repositoryIds.join(\",\"),\n [factoryReplyChannelHeader]: input.replyTo.channel,\n [factoryReplyAddressHeader]: input.replyTo.address,\n };\n}\n\n/** Add a validated Task ID and attempt fence to an authenticated Eve initiator. */\nexport function taskSessionAuth(\n auth: AuthenticatedSessionInitiator,\n task: TaskAttemptRef,\n): AuthenticatedSessionInitiator {\n return {\n ...auth,\n attributes: {\n ...auth.attributes,\n [factoryTaskAttribute]: task.taskId,\n [factoryTaskAttemptAttribute]: String(task.attempt),\n },\n };\n}\n\n/**\n * Requires valid Factory Task headers and binds them into an authenticated Eve initiator.\n *\n * @remarks\n * The wrapped authenticator runs first. If it returns no identity, that result is preserved. For\n * an authenticated caller, both Factory headers are mandatory and validated before their Task ID\n * and attempt are copied into session attributes. This records claimed identity; mutating tools\n * call `requireTaskExecution` to recheck it against current durable Task state.\n *\n * Mount this wrapper only on routes reached through a trusted launcher that supplies\n * `taskHeaders`. It does not independently verify that the caller may claim an arbitrary Task.\n *\n * @param authenticate - Existing Eve request authenticator for the trusted launcher.\n * @returns An authenticator that adds the validated Factory Task binding.\n * @throws `ForbiddenError` when either Task header is absent or malformed.\n *\n * @example\n * ```ts\n * import { withFactoryTask } from \"@vercel/factory\";\n * import { localDev } from \"eve/channels/auth\";\n * import { eveChannel } from \"eve/channels/eve\";\n *\n * export default eveChannel({ auth: [withFactoryTask(localDev())] });\n * ```\n */\nexport function withFactoryTask(authenticate: AuthFn<Request>): AuthFn<Request> {\n return async (request) => {\n const auth = await authenticate(request);\n if (auth == null) return auth;\n const parsedTask = taskIdSchema.safeParse(request.headers.get(factoryTaskHeader));\n const parsedAttempt = parseTaskAttempt(request.headers.get(factoryTaskAttemptHeader));\n if (!parsedTask.success || parsedAttempt === null) throw new ForbiddenError();\n return taskSessionAuth(auth, taskAttemptRef(parsedTask.data, parsedAttempt));\n };\n}\n\n/** Bind optional Task and conversation headers while rejecting partial or malformed context. */\nexport function withOptionalFactoryTask(authenticate: AuthFn<Request>): AuthFn<Request> {\n return async (request) => {\n const auth = await authenticate(request);\n if (auth == null) return auth;\n const taskId = request.headers.get(factoryTaskHeader);\n const taskAttempt = request.headers.get(factoryTaskAttemptHeader);\n const repositoryIds = request.headers.get(factoryRepositoryIdsHeader);\n const replyChannel = request.headers.get(factoryReplyChannelHeader);\n const replyAddress = request.headers.get(factoryReplyAddressHeader);\n const hasConversationBinding =\n repositoryIds !== null || replyChannel !== null || replyAddress !== null;\n if (taskId === null && taskAttempt === null && !hasConversationBinding) return auth;\n const parsedTask = taskId === null ? null : taskIdSchema.safeParse(taskId);\n const parsedAttempt = parseTaskAttempt(taskAttempt);\n const parsedRepositories =\n repositoryIds === null\n ? null\n : repositoryIdSchema.array().min(1).safeParse(repositoryIds.split(\",\").filter(Boolean));\n const parsedReplyTo =\n replyChannel === null || replyAddress === null\n ? null\n : replyToSchema.safeParse({ channel: replyChannel, address: replyAddress });\n if (\n (parsedTask !== null && !parsedTask.success) ||\n (parsedTask === null) !== (parsedAttempt === null) ||\n (hasConversationBinding &&\n (parsedRepositories === null ||\n !parsedRepositories.success ||\n parsedReplyTo === null ||\n !parsedReplyTo.success))\n ) {\n throw new ForbiddenError();\n }\n const conversationAttributes: Record<string, string> =\n parsedRepositories?.success === true && parsedReplyTo?.success === true\n ? {\n [factoryRepositoryIdsAttribute]: parsedRepositories.data.join(\",\"),\n [factoryReplyChannelAttribute]: parsedReplyTo.data.channel,\n [factoryReplyAddressAttribute]: parsedReplyTo.data.address,\n }\n : {};\n return {\n ...auth,\n attributes: {\n ...auth.attributes,\n ...(parsedTask === null || parsedAttempt === null\n ? {}\n : {\n [factoryTaskAttribute]: parsedTask.data,\n [factoryTaskAttemptAttribute]: String(parsedAttempt),\n }),\n ...conversationAttributes,\n },\n };\n };\n}\n\n/** Read the required Factory Task ID from authenticated Eve session state. */\nexport function requireTaskIdForSession(auth: SessionInitiator): TaskId {\n return taskIdSchema.parse(auth?.attributes[factoryTaskAttribute]);\n}\n\n/** Read an optional Task ID from authenticated session state, rejecting malformed values. */\nexport function optionalTaskIdForSession(auth: SessionInitiator): TaskId | null {\n const value = auth?.attributes[factoryTaskAttribute];\n return value === undefined ? null : taskIdSchema.parse(value);\n}\n\n/** Read the complete, attempt-fenced Task binding established by Factory authentication. */\nexport function taskBindingForSession(auth: SessionInitiator): TaskAttemptRef {\n const taskId = requireTaskIdForSession(auth);\n const rawAttempt = auth?.attributes[factoryTaskAttemptAttribute];\n const attempt = parseTaskAttempt(typeof rawAttempt === \"string\" ? rawAttempt : null);\n if (attempt === null) throw new Error(\"Factory session Task attempt binding is invalid.\");\n return { taskId, attempt };\n}\n\n/** Read the repository authorization envelope, or null when the session has none. */\nexport function repositoryIdsForSession(auth: SessionInitiator): readonly RepositoryId[] | null {\n const value = auth?.attributes[factoryRepositoryIdsAttribute];\n if (value === undefined) return null;\n return repositoryIdSchema\n .array()\n .min(1)\n .parse(typeof value === \"string\" ? value.split(\",\").filter(Boolean) : value);\n}\n\n/** Read the authenticated reply destination, or null when the session has none. */\nexport function replyToForSession(auth: SessionInitiator): ReplyTo | null {\n const channel = auth?.attributes[factoryReplyChannelAttribute];\n const address = auth?.attributes[factoryReplyAddressAttribute];\n if (channel === undefined && address === undefined) return null;\n return replyToSchema.parse({ channel, address });\n}\n\n/** Read the complete conversation binding, rejecting partial authentication state. */\nexport function conversationBindingForSession(\n auth: SessionInitiator,\n): FactoryConversationSessionBinding | null {\n const repositoryIds = repositoryIdsForSession(auth);\n const replyTo = replyToForSession(auth);\n if (repositoryIds === null && replyTo === null) return null;\n if (repositoryIds === null || replyTo === null) {\n throw new Error(\"Factory session conversation binding is incomplete.\");\n }\n return { repositoryIds, replyTo };\n}\n\n/** Authenticated conversation authority assembled from both initiator and current session state. */\nexport interface ConversationBinding {\n replyTo: ReplyTo;\n /**\n * Repository envelopes the binding carries: the initiator's, plus the current\n * auth's when it restates one. Admitted work must sit inside every envelope.\n */\n repositoryEnvelopes: readonly (readonly RepositoryId[])[];\n parentTask?: TaskAttemptRef;\n}\n\nconst conversationBindingAttributes = [\n factoryReplyChannelAttribute,\n factoryReplyAddressAttribute,\n factoryTaskAttribute,\n factoryTaskAttemptAttribute,\n] as const;\n\n/**\n * Read and validate the conversation binding carried on a session's auth pair.\n * The initiator's attributes are authoritative; any binding attribute the\n * current auth restates must agree with them. This is the only reader of the\n * binding attribute encoding; callers never touch `attributes` themselves.\n *\n * Distinct from `conversationBindingForSession`, which reads the initiator\n * alone: admission needs every envelope the pair carries, so that work cannot\n * be admitted into a repository only one side of the pair authorizes.\n */\nexport function conversationBinding(auth: {\n initiator: AuthenticatedSessionInitiator;\n current: AuthenticatedSessionInitiator;\n}): ConversationBinding {\n const attributes = auth.initiator.attributes;\n const replyTo = replyToSchema.parse({\n channel: attributes[factoryReplyChannelAttribute],\n address: attributes[factoryReplyAddressAttribute],\n });\n for (const key of conversationBindingAttributes) {\n if (\n auth.current.attributes[key] !== undefined &&\n auth.current.attributes[key] !== attributes[key]\n )\n throw new Error(\"Conversation binding does not match the authenticated conversation\");\n }\n const repositoryEnvelopes: (readonly RepositoryId[])[] = [];\n for (const [index, entry] of [auth.initiator, auth.current].entries()) {\n const raw = entry.attributes[factoryRepositoryIdsAttribute];\n if (index === 1 && raw === undefined) continue;\n repositoryEnvelopes.push(\n repositoryIdSchema\n .array()\n .min(1)\n .parse(typeof raw === \"string\" ? raw.split(\",\") : raw),\n );\n }\n let parentTask: ConversationBinding[\"parentTask\"];\n if (\n attributes[factoryTaskAttribute] !== undefined ||\n attributes[factoryTaskAttemptAttribute] !== undefined\n ) {\n const taskId = taskIdSchema.parse(attributes[factoryTaskAttribute]);\n const rawAttempt = attributes[factoryTaskAttemptAttribute];\n const attempt = typeof rawAttempt === \"string\" ? parseTaskAttempt(rawAttempt) : null;\n if (attempt === null) throw new Error(\"Conversation binding carries an invalid parent attempt\");\n parentTask = taskAttemptRef(taskId, attempt);\n }\n return { replyTo, repositoryEnvelopes, parentTask };\n}\n\nfunction parseTaskAttempt(value: string | null): number | null {\n if (value === null || !/^[1-9]\\d*$/u.test(value)) return null;\n const attempt = Number(value);\n return Number.isSafeInteger(attempt) ? attempt : null;\n}\n"],"mappings":"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"}
@@ -39,6 +39,7 @@ function createEveTranscriptMaterializer(options) {
39
39
  });
40
40
  const origin = typeof options.origin === "string" ? options.origin : options.origin();
41
41
  const response = await fetchImplementation(`${origin.replace(/\/+$/u, "")}/eve/agents/${encodeURIComponent(input.agent)}/eve/v1/session/${encodeURIComponent(input.execution.sessionId)}/stream?${query}`, {
42
+ cache: "no-store",
42
43
  headers: await options.headers?.(scopeFor(input)),
43
44
  signal: AbortSignal.timeout(timeoutMs),
44
45
  redirect: "error"
@@ -53,7 +54,10 @@ function createEveTranscriptMaterializer(options) {
53
54
  await response.body.cancel();
54
55
  throw new Error("Eve transcript materialization did not include a valid durable cursor");
55
56
  }
56
- if (transcript.cursor <= tail) for await (const event of readEvents(response.body)) transcript.accept(event);
57
+ if (transcript.cursor <= tail) for await (const event of readEvents(response.body)) {
58
+ transcript.accept(event);
59
+ if (transcript.cursor === tail + 1) break;
60
+ }
57
61
  else await response.body.cancel();
58
62
  if (transcript.cursor !== tail + 1) throw new Error("Eve transcript materialization closed before its durable cursor");
59
63
  await options.stores.transcripts.put(input.execution, transcript.snapshot());
@@ -1 +1 @@
1
- {"version":3,"file":"transcript.mjs","names":[],"sources":["../../src/eve/transcript.ts"],"sourcesContent":["import { readEvents } from \"../client-events\";\nimport { Transcript } from \"../client-transcript\";\nimport { replyToSchema, type MaybePromise, type ReplyTo } from \"../schema/common\";\nimport { taskAttemptRef, type TaskAttemptRef } from \"../schema/execution\";\nimport { repositoryIdSchema, taskIdSchema, type RepositoryId } from \"../schema/id\";\nimport type { FactoryStores } from \"../store/engine\";\nimport {\n factoryReplyAddressAttribute,\n factoryReplyChannelAttribute,\n factoryRepositoryIdsAttribute,\n factoryTaskAttemptAttribute,\n factoryTaskAttribute,\n} from \"./task-session\";\n\n/** Identifies the Eve execution and authenticated attributes to project into a transcript. */\nexport interface TranscriptMaterializationInput {\n agent: string;\n execution: { provider: \"eve\"; sessionId: string };\n attributes: Readonly<Record<string, string | readonly string[]>>;\n}\n\n/** The Task and conversation authorization context forwarded when reading an Eve transcript. */\nexport interface EveTranscriptRequestScope {\n task?: TaskAttemptRef;\n conversation?: { repositoryIds: readonly RepositoryId[]; replyTo: ReplyTo };\n}\n\n/** Configures durable transcript storage and authenticated access to Eve session events. */\nexport interface EveTranscriptMaterializerOptions {\n stores: Pick<FactoryStores, \"transcripts\">;\n origin: string | (() => string);\n headers?(scope: EveTranscriptRequestScope): MaybePromise<HeadersInit>;\n fetch?: typeof globalThis.fetch;\n timeoutMs?: number;\n}\n\n/** Materializes one Eve execution stream into Factory's browser-safe transcript store. */\nexport type EveTranscriptMaterializer = (input: TranscriptMaterializationInput) => Promise<void>;\n\nfunction attribute(\n attributes: TranscriptMaterializationInput[\"attributes\"],\n name: string,\n): string | undefined {\n const value = attributes[name];\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction scopeFor(input: TranscriptMaterializationInput): EveTranscriptRequestScope {\n const taskId = taskIdSchema.safeParse(attribute(input.attributes, factoryTaskAttribute));\n const attempt = Number(attribute(input.attributes, factoryTaskAttemptAttribute));\n const repositories = repositoryIdSchema\n .array()\n .min(1)\n .safeParse(\n attribute(input.attributes, factoryRepositoryIdsAttribute)?.split(\",\").filter(Boolean),\n );\n const replyTo = replyToSchema.safeParse({\n channel: attribute(input.attributes, factoryReplyChannelAttribute),\n address: attribute(input.attributes, factoryReplyAddressAttribute),\n });\n return {\n ...(taskId.success && Number.isSafeInteger(attempt) && attempt > 0\n ? { task: taskAttemptRef(taskId.data, attempt) }\n : {}),\n ...(repositories.success && replyTo.success\n ? { conversation: { repositoryIds: repositories.data, replyTo: replyTo.data } }\n : {}),\n };\n}\n\n/** Materialize a browser-safe transcript projection from one durable Eve session stream. */\nexport function createEveTranscriptMaterializer(\n options: EveTranscriptMaterializerOptions,\n): EveTranscriptMaterializer {\n const fetchImplementation = options.fetch ?? globalThis.fetch;\n const timeoutMs = options.timeoutMs ?? 30_000;\n return async (input: TranscriptMaterializationInput): Promise<void> => {\n const previous = await options.stores.transcripts.get(input.execution);\n const transcript = new Transcript();\n if (previous !== null) transcript.hydrate(previous);\n const query = new URLSearchParams({\n startIndex: String(transcript.cursor),\n includeTailIndex: \"1\",\n });\n const origin = typeof options.origin === \"string\" ? options.origin : options.origin();\n const response = await fetchImplementation(\n `${origin.replace(/\\/+$/u, \"\")}/eve/agents/${encodeURIComponent(input.agent)}/eve/v1/session/${encodeURIComponent(input.execution.sessionId)}/stream?${query}`,\n {\n headers: await options.headers?.(scopeFor(input)),\n signal: AbortSignal.timeout(timeoutMs),\n redirect: \"error\",\n },\n );\n if (!response.ok || !response.body) {\n await response.body?.cancel();\n throw new Error(`Eve refused transcript materialization: ${response.status}`);\n }\n const tailValue = response.headers.get(\"x-eve-stream-tail-index\");\n const tail = tailValue === null ? Number.NaN : Number(tailValue);\n if (!Number.isSafeInteger(tail) || tail < -1) {\n await response.body.cancel();\n throw new Error(\"Eve transcript materialization did not include a valid durable cursor\");\n }\n if (transcript.cursor <= tail) {\n for await (const event of readEvents(response.body)) transcript.accept(event);\n } else {\n await response.body.cancel();\n }\n if (transcript.cursor !== tail + 1) {\n throw new Error(\"Eve transcript materialization closed before its durable cursor\");\n }\n await options.stores.transcripts.put(input.execution, transcript.snapshot());\n };\n}\n"],"mappings":";;;;;;;AAuCA,SAAS,UACP,YACA,MACoB;CACpB,MAAM,QAAQ,WAAW;CACzB,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;AAEA,SAAS,SAAS,OAAkE;CAClF,MAAM,SAAS,aAAa,UAAU,UAAU,MAAM,YAAY,oBAAoB,CAAC;CACvF,MAAM,UAAU,OAAO,UAAU,MAAM,YAAY,2BAA2B,CAAC;CAC/E,MAAM,eAAe,mBAClB,MAAM,CAAC,CACP,IAAI,CAAC,CAAC,CACN,UACC,UAAU,MAAM,YAAY,6BAA6B,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CACvF;CACF,MAAM,UAAU,cAAc,UAAU;EACtC,SAAS,UAAU,MAAM,YAAY,4BAA4B;EACjE,SAAS,UAAU,MAAM,YAAY,4BAA4B;CACnE,CAAC;CACD,OAAO;EACL,GAAI,OAAO,WAAW,OAAO,cAAc,OAAO,KAAK,UAAU,IAC7D,EAAE,MAAM,eAAe,OAAO,MAAM,OAAO,EAAE,IAC7C,CAAC;EACL,GAAI,aAAa,WAAW,QAAQ,UAChC,EAAE,cAAc;GAAE,eAAe,aAAa;GAAM,SAAS,QAAQ;EAAK,EAAE,IAC5E,CAAC;CACP;AACF;;AAGA,SAAgB,gCACd,SAC2B;CAC3B,MAAM,sBAAsB,QAAQ,SAAS,WAAW;CACxD,MAAM,YAAY,QAAQ,aAAa;CACvC,OAAO,OAAO,UAAyD;EACrE,MAAM,WAAW,MAAM,QAAQ,OAAO,YAAY,IAAI,MAAM,SAAS;EACrE,MAAM,aAAa,IAAI,WAAW;EAClC,IAAI,aAAa,MAAM,WAAW,QAAQ,QAAQ;EAClD,MAAM,QAAQ,IAAI,gBAAgB;GAChC,YAAY,OAAO,WAAW,MAAM;GACpC,kBAAkB;EACpB,CAAC;EACD,MAAM,SAAS,OAAO,QAAQ,WAAW,WAAW,QAAQ,SAAS,QAAQ,OAAO;EACpF,MAAM,WAAW,MAAM,oBACrB,GAAG,OAAO,QAAQ,SAAS,EAAE,EAAE,cAAc,mBAAmB,MAAM,KAAK,EAAE,kBAAkB,mBAAmB,MAAM,UAAU,SAAS,EAAE,UAAU,SACvJ;GACE,SAAS,MAAM,QAAQ,UAAU,SAAS,KAAK,CAAC;GAChD,QAAQ,YAAY,QAAQ,SAAS;GACrC,UAAU;EACZ,CACF;EACA,IAAI,CAAC,SAAS,MAAM,CAAC,SAAS,MAAM;GAClC,MAAM,SAAS,MAAM,OAAO;GAC5B,MAAM,IAAI,MAAM,2CAA2C,SAAS,QAAQ;EAC9E;EACA,MAAM,YAAY,SAAS,QAAQ,IAAI,yBAAyB;EAChE,MAAM,OAAO,cAAc,OAAO,MAAa,OAAO,SAAS;EAC/D,IAAI,CAAC,OAAO,cAAc,IAAI,KAAK,OAAO,IAAI;GAC5C,MAAM,SAAS,KAAK,OAAO;GAC3B,MAAM,IAAI,MAAM,uEAAuE;EACzF;EACA,IAAI,WAAW,UAAU,MACvB,WAAW,MAAM,SAAS,WAAW,SAAS,IAAI,GAAG,WAAW,OAAO,KAAK;OAE5E,MAAM,SAAS,KAAK,OAAO;EAE7B,IAAI,WAAW,WAAW,OAAO,GAC/B,MAAM,IAAI,MAAM,iEAAiE;EAEnF,MAAM,QAAQ,OAAO,YAAY,IAAI,MAAM,WAAW,WAAW,SAAS,CAAC;CAC7E;AACF"}
1
+ {"version":3,"file":"transcript.mjs","names":[],"sources":["../../src/eve/transcript.ts"],"sourcesContent":["import { readEvents } from \"../client-events\";\nimport { Transcript } from \"../client-transcript\";\nimport { replyToSchema, type MaybePromise, type ReplyTo } from \"../schema/common\";\nimport { taskAttemptRef, type TaskAttemptRef } from \"../schema/execution\";\nimport { repositoryIdSchema, taskIdSchema, type RepositoryId } from \"../schema/id\";\nimport type { FactoryStores } from \"../store/engine\";\nimport {\n factoryReplyAddressAttribute,\n factoryReplyChannelAttribute,\n factoryRepositoryIdsAttribute,\n factoryTaskAttemptAttribute,\n factoryTaskAttribute,\n} from \"./task-session\";\n\n/** Identifies the Eve execution and authenticated attributes to project into a transcript. */\nexport interface TranscriptMaterializationInput {\n agent: string;\n execution: { provider: \"eve\"; sessionId: string };\n attributes: Readonly<Record<string, string | readonly string[]>>;\n}\n\n/** The Task and conversation authorization context forwarded when reading an Eve transcript. */\nexport interface EveTranscriptRequestScope {\n task?: TaskAttemptRef;\n conversation?: { repositoryIds: readonly RepositoryId[]; replyTo: ReplyTo };\n}\n\n/** Configures durable transcript storage and authenticated access to Eve session events. */\nexport interface EveTranscriptMaterializerOptions {\n stores: Pick<FactoryStores, \"transcripts\">;\n origin: string | (() => string);\n headers?(scope: EveTranscriptRequestScope): MaybePromise<HeadersInit>;\n fetch?: typeof globalThis.fetch;\n timeoutMs?: number;\n}\n\n/** Materializes one Eve execution stream into Factory's browser-safe transcript store. */\nexport type EveTranscriptMaterializer = (input: TranscriptMaterializationInput) => Promise<void>;\n\nfunction attribute(\n attributes: TranscriptMaterializationInput[\"attributes\"],\n name: string,\n): string | undefined {\n const value = attributes[name];\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction scopeFor(input: TranscriptMaterializationInput): EveTranscriptRequestScope {\n const taskId = taskIdSchema.safeParse(attribute(input.attributes, factoryTaskAttribute));\n const attempt = Number(attribute(input.attributes, factoryTaskAttemptAttribute));\n const repositories = repositoryIdSchema\n .array()\n .min(1)\n .safeParse(\n attribute(input.attributes, factoryRepositoryIdsAttribute)?.split(\",\").filter(Boolean),\n );\n const replyTo = replyToSchema.safeParse({\n channel: attribute(input.attributes, factoryReplyChannelAttribute),\n address: attribute(input.attributes, factoryReplyAddressAttribute),\n });\n return {\n ...(taskId.success && Number.isSafeInteger(attempt) && attempt > 0\n ? { task: taskAttemptRef(taskId.data, attempt) }\n : {}),\n ...(repositories.success && replyTo.success\n ? { conversation: { repositoryIds: repositories.data, replyTo: replyTo.data } }\n : {}),\n };\n}\n\n/** Materialize a browser-safe transcript projection from one durable Eve session stream. */\nexport function createEveTranscriptMaterializer(\n options: EveTranscriptMaterializerOptions,\n): EveTranscriptMaterializer {\n const fetchImplementation = options.fetch ?? globalThis.fetch;\n const timeoutMs = options.timeoutMs ?? 30_000;\n return async (input: TranscriptMaterializationInput): Promise<void> => {\n const previous = await options.stores.transcripts.get(input.execution);\n const transcript = new Transcript();\n if (previous !== null) transcript.hydrate(previous);\n const query = new URLSearchParams({\n startIndex: String(transcript.cursor),\n includeTailIndex: \"1\",\n });\n const origin = typeof options.origin === \"string\" ? options.origin : options.origin();\n const response = await fetchImplementation(\n `${origin.replace(/\\/+$/u, \"\")}/eve/agents/${encodeURIComponent(input.agent)}/eve/v1/session/${encodeURIComponent(input.execution.sessionId)}/stream?${query}`,\n {\n cache: \"no-store\",\n headers: await options.headers?.(scopeFor(input)),\n signal: AbortSignal.timeout(timeoutMs),\n redirect: \"error\",\n },\n );\n if (!response.ok || !response.body) {\n await response.body?.cancel();\n throw new Error(`Eve refused transcript materialization: ${response.status}`);\n }\n const tailValue = response.headers.get(\"x-eve-stream-tail-index\");\n const tail = tailValue === null ? Number.NaN : Number(tailValue);\n if (!Number.isSafeInteger(tail) || tail < -1) {\n await response.body.cancel();\n throw new Error(\"Eve transcript materialization did not include a valid durable cursor\");\n }\n if (transcript.cursor <= tail) {\n for await (const event of readEvents(response.body)) {\n transcript.accept(event);\n if (transcript.cursor === tail + 1) break;\n }\n } else {\n await response.body.cancel();\n }\n if (transcript.cursor !== tail + 1) {\n throw new Error(\"Eve transcript materialization closed before its durable cursor\");\n }\n await options.stores.transcripts.put(input.execution, transcript.snapshot());\n };\n}\n"],"mappings":";;;;;;;AAuCA,SAAS,UACP,YACA,MACoB;CACpB,MAAM,QAAQ,WAAW;CACzB,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;AAEA,SAAS,SAAS,OAAkE;CAClF,MAAM,SAAS,aAAa,UAAU,UAAU,MAAM,YAAY,oBAAoB,CAAC;CACvF,MAAM,UAAU,OAAO,UAAU,MAAM,YAAY,2BAA2B,CAAC;CAC/E,MAAM,eAAe,mBAClB,MAAM,CAAC,CACP,IAAI,CAAC,CAAC,CACN,UACC,UAAU,MAAM,YAAY,6BAA6B,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CACvF;CACF,MAAM,UAAU,cAAc,UAAU;EACtC,SAAS,UAAU,MAAM,YAAY,4BAA4B;EACjE,SAAS,UAAU,MAAM,YAAY,4BAA4B;CACnE,CAAC;CACD,OAAO;EACL,GAAI,OAAO,WAAW,OAAO,cAAc,OAAO,KAAK,UAAU,IAC7D,EAAE,MAAM,eAAe,OAAO,MAAM,OAAO,EAAE,IAC7C,CAAC;EACL,GAAI,aAAa,WAAW,QAAQ,UAChC,EAAE,cAAc;GAAE,eAAe,aAAa;GAAM,SAAS,QAAQ;EAAK,EAAE,IAC5E,CAAC;CACP;AACF;;AAGA,SAAgB,gCACd,SAC2B;CAC3B,MAAM,sBAAsB,QAAQ,SAAS,WAAW;CACxD,MAAM,YAAY,QAAQ,aAAa;CACvC,OAAO,OAAO,UAAyD;EACrE,MAAM,WAAW,MAAM,QAAQ,OAAO,YAAY,IAAI,MAAM,SAAS;EACrE,MAAM,aAAa,IAAI,WAAW;EAClC,IAAI,aAAa,MAAM,WAAW,QAAQ,QAAQ;EAClD,MAAM,QAAQ,IAAI,gBAAgB;GAChC,YAAY,OAAO,WAAW,MAAM;GACpC,kBAAkB;EACpB,CAAC;EACD,MAAM,SAAS,OAAO,QAAQ,WAAW,WAAW,QAAQ,SAAS,QAAQ,OAAO;EACpF,MAAM,WAAW,MAAM,oBACrB,GAAG,OAAO,QAAQ,SAAS,EAAE,EAAE,cAAc,mBAAmB,MAAM,KAAK,EAAE,kBAAkB,mBAAmB,MAAM,UAAU,SAAS,EAAE,UAAU,SACvJ;GACE,OAAO;GACP,SAAS,MAAM,QAAQ,UAAU,SAAS,KAAK,CAAC;GAChD,QAAQ,YAAY,QAAQ,SAAS;GACrC,UAAU;EACZ,CACF;EACA,IAAI,CAAC,SAAS,MAAM,CAAC,SAAS,MAAM;GAClC,MAAM,SAAS,MAAM,OAAO;GAC5B,MAAM,IAAI,MAAM,2CAA2C,SAAS,QAAQ;EAC9E;EACA,MAAM,YAAY,SAAS,QAAQ,IAAI,yBAAyB;EAChE,MAAM,OAAO,cAAc,OAAO,MAAa,OAAO,SAAS;EAC/D,IAAI,CAAC,OAAO,cAAc,IAAI,KAAK,OAAO,IAAI;GAC5C,MAAM,SAAS,KAAK,OAAO;GAC3B,MAAM,IAAI,MAAM,uEAAuE;EACzF;EACA,IAAI,WAAW,UAAU,MACvB,WAAW,MAAM,SAAS,WAAW,SAAS,IAAI,GAAG;GACnD,WAAW,OAAO,KAAK;GACvB,IAAI,WAAW,WAAW,OAAO,GAAG;EACtC;OAEA,MAAM,SAAS,KAAK,OAAO;EAE7B,IAAI,WAAW,WAAW,OAAO,GAC/B,MAAM,IAAI,MAAM,iEAAiE;EAEnF,MAAM,QAAQ,OAAO,YAAY,IAAI,MAAM,WAAW,WAAW,SAAS,CAAC;CAC7E;AACF"}
@@ -4,28 +4,56 @@ import { FactoryStores } from "./store/engine.mjs";
4
4
  //#region src/execution.d.ts
5
5
  /** Validated request and stable idempotency key passed to an execution provider. */
6
6
  interface StartExecutionInput {
7
+ /** Exact Task-attempt request validated before the provider is called. */
7
8
  readonly request: CodingExecutionRequest;
9
+ /** Stable key that must identify the same provider launch across retries and process restarts. */
8
10
  readonly launchKey: ExecutionLaunchKey;
9
11
  }
10
12
  /** Current provider state when inspecting one recorded execution. */
11
13
  type ExecutionInspectionResult = {
14
+ /** The recorded execution still exists and is safe for the workflow to reattach. */
12
15
  status: "active";
13
16
  } | {
17
+ /** The provider can no longer inspect or reattach the recorded execution. */
14
18
  status: "expired";
15
19
  } | {
20
+ /** The provider has immutable final evidence for the recorded execution. */
16
21
  status: "finished";
17
22
  result: ExecutionResult;
18
23
  };
19
24
  /** Provider-neutral boundary for retry-safe execution start, inspection, and cancellation. */
20
25
  interface ExecutionAdapter {
21
- /** Retry-safe by key. Throw ExecutionLaunchPendingError only for ambiguous acceptance; other errors mean no launch. */
26
+ /**
27
+ * Starts or recovers one launch by its stable key. Return only after provider acceptance is
28
+ * known. Throw `ExecutionLaunchPendingError` when acceptance is ambiguous; every other rejection
29
+ * promises that this call did not leave an untracked launch.
30
+ *
31
+ * @param input - Validated provider request and stable launch key.
32
+ * @returns The accepted execution reference for this logical launch.
33
+ */
22
34
  start(input: StartExecutionInput): Promise<ExecutionReference>;
23
- /** Only active confirms that reattachment is safe. Missing/expired execution must never start implicitly. */
35
+ /**
36
+ * Observes one exact provider handle without starting replacement work. Only `active` confirms
37
+ * that reattachment is safe; `expired` and `finished` require workflow-owned reconciliation.
38
+ *
39
+ * @param execution - Exact provider handle previously recorded on the Task.
40
+ * @returns Current provider state and final result when finished.
41
+ */
24
42
  inspect(execution: ExecutionReference): Promise<ExecutionInspectionResult>;
25
- /** Idempotent stop, returning final observed usage. May report cancellation even if interruption is unsupported. */
43
+ /**
44
+ * Idempotently requests a stop and returns the provider's final observed result and usage. An
45
+ * adapter may report cancellation even when immediate interruption is unsupported; callers that
46
+ * require proof of inactivity must use provider-specific confirmation.
47
+ *
48
+ * @param execution - Exact provider handle to stop or confirm inactive.
49
+ * @returns Immutable final result and usage for that execution.
50
+ */
26
51
  stop(execution: ExecutionReference): Promise<ExecutionResult>;
27
52
  }
28
- /** Error raised when an execution no longer matches the current running Task attempt. */
53
+ /**
54
+ * Error raised when an execution no longer matches the current running Task attempt.
55
+ * Reread the Task and discard the stale observation; do not apply it to the replacement attempt.
56
+ */
29
57
  declare class ExecutionFenceError extends Error {}
30
58
  /** Store capabilities required to launch or reattach a coding execution. */
31
59
  type LaunchExecutionStores = Pick<FactoryStores, "repositories" | "tasks">;
@@ -35,40 +63,120 @@ type InspectExecutionStores = Pick<FactoryStores, "tasks">;
35
63
  type RecordExecutionUsageStores = Pick<FactoryStores, "receipts" | "tasks">;
36
64
  /** Store capabilities required to cancel and settle a coding execution. */
37
65
  type CancelExecutionStores = RecordExecutionUsageStores;
38
- /** Construct the stable provider idempotency key for one exact Task attempt. */
66
+ /**
67
+ * Constructs the stable provider idempotency key for one exact Task attempt.
68
+ * Persist or deterministically recreate this key across launch recovery; a new attempt gets a new key.
69
+ */
39
70
  declare function executionLaunchKey(ref: TaskAttemptRef): ExecutionLaunchKey;
40
71
  /** Dependencies and request used to start or reattach one coding execution. */
41
72
  interface LaunchExecutionOptions {
73
+ /** Task and repository reads plus the durable execution-binding write. */
42
74
  readonly stores: LaunchExecutionStores;
75
+ /** Exact running Task attempt and single repository supplied to the provider. */
43
76
  readonly request: CodingExecutionRequest;
77
+ /** Provider boundary responsible for idempotent start and exact-handle inspection. */
44
78
  readonly adapter: ExecutionAdapter;
45
79
  }
46
- /** Called inside a durable workflow step after dispatch gates. No provider lifecycle is stored here. */
80
+ /**
81
+ * Starts or reattaches one coding execution after dispatch has admitted its Task.
82
+ *
83
+ * @remarks
84
+ * The Task must still be `running` on the requested attempt and name exactly one enabled matching
85
+ * repository. If an execution is already recorded, this method inspects it and returns it only
86
+ * when the provider confirms it is active; it never starts an implicit replacement for a finished
87
+ * or expired handle.
88
+ *
89
+ * For a new launch, the adapter receives a stable key derived from the Task attempt. The method
90
+ * returns only after the provider reference is durably bound to that attempt. If acceptance or
91
+ * the binding write is ambiguous, it throws `ExecutionLaunchPendingError`; retry this operation
92
+ * with the same request so the same key can recover the launch. Reread the Task before applying
93
+ * any other error, because a concurrent writer may have replaced the attempt.
94
+ *
95
+ * @param options - Validated request, required stores, and provider adapter.
96
+ * @returns The recorded provider execution for the current running attempt.
97
+ * @throws `ExecutionFenceError` when Task attempt or execution ownership changes.
98
+ * @throws `ExecutionLaunchPendingError` when provider acceptance or durable binding is ambiguous.
99
+ */
47
100
  declare function launchExecution(options: LaunchExecutionOptions): Promise<ExecutionReference>;
48
101
  /** Dependencies and Task-attempt identity used to inspect one execution. */
49
102
  interface InspectExecutionOptions {
103
+ /** Task read used to establish and recheck the execution fence. */
50
104
  readonly stores: InspectExecutionStores;
105
+ /** Exact running Task attempt whose recorded execution may be inspected. */
51
106
  readonly task: TaskAttemptRef;
107
+ /** Provider boundary used only for observation. */
52
108
  readonly adapter: ExecutionAdapter;
53
109
  }
54
- /** Read a result without granting it lifecycle authority; workflows validate evidence and choose the next state. */
110
+ /**
111
+ * Inspects a recorded execution without granting provider evidence lifecycle authority.
112
+ *
113
+ * @remarks
114
+ * The Task must be running on the exact attempt and have a recorded execution. Ownership is
115
+ * checked again after the provider call so a concurrent replacement cannot attach stale results.
116
+ * Finished results are schema-validated and must identify that same execution. This method writes
117
+ * no Task state or usage; the workflow decides how to validate evidence and transition afterward.
118
+ *
119
+ * @param options - Exact Task attempt, Task store, and provider adapter.
120
+ * @returns Current provider state and a validated result when finished.
121
+ * @throws `ExecutionFenceError` when the attempt or execution changes during inspection.
122
+ */
55
123
  declare function inspectExecution(options: InspectExecutionOptions): Promise<ExecutionInspectionResult>;
56
124
  /** Dependencies, Task-attempt identity, and provider result used to record usage. */
57
125
  interface RecordExecutionUsageOptions {
126
+ /** Receipt and Task stores used for idempotent usage persistence. */
58
127
  readonly stores: RecordExecutionUsageStores;
128
+ /** Exact Task attempt that owns the provider result. */
59
129
  readonly task: TaskAttemptRef;
130
+ /** Final provider result whose execution must match the Task's recorded handle. */
60
131
  readonly result: ExecutionResult;
61
132
  }
62
- /** For adapters reporting one immutable final usage packet instead of per-step usage hooks. */
133
+ /**
134
+ * Records one adapter's immutable final usage packet without changing Task lifecycle state.
135
+ *
136
+ * @remarks
137
+ * Known cost is delegated to the Task usage store under a deterministic turn key, making identical
138
+ * replay non-charging. This API records an immutable final usage packet; an adapter must not also
139
+ * report the same charges through per-step usage hooks, because those observations use independent
140
+ * idempotency keys and would be added twice. When cost is unknown, an idempotent `usage_unresolved`
141
+ * receipt is appended and the Task remains unsettled so its reservation is retained. The workflow
142
+ * or reconciler must later supply measured usage before settlement.
143
+ *
144
+ * @param options - Exact Task attempt, final provider result, and persistence stores.
145
+ * @returns The Task after usage persistence, or unchanged after unresolved-cost evidence.
146
+ * @throws `ExecutionFenceError` when the result belongs to another attempt or execution.
147
+ */
63
148
  declare function recordExecutionUsage(options: RecordExecutionUsageOptions): Promise<Task>;
64
149
  /** Dependencies and reason used to cancel one exact Task execution attempt. */
65
150
  interface CancelExecutionOptions {
151
+ /** Task and receipt stores used for cancellation, usage, and settlement. */
66
152
  readonly stores: CancelExecutionStores;
153
+ /** Exact attempt to cancel; replacement attempts are refused. */
67
154
  readonly task: TaskAttemptRef;
155
+ /** Provider boundary used to request an idempotent stop and read its final observed result. */
68
156
  readonly adapter: ExecutionAdapter;
157
+ /** Human-readable cancellation reason retained in lifecycle evidence. */
69
158
  readonly reason: string;
70
159
  }
71
- /** Canonical cancellation precedes provider interruption. Retry this same call after an ambiguous stop. */
160
+ /**
161
+ * Cancels canonical Task work, then interrupts and settles its exact provider execution.
162
+ *
163
+ * @remarks
164
+ * The Task first durably enters `cancelled`, preventing later workflow completion from winning.
165
+ * Provider interruption happens afterward and cannot roll that transition back. An absent recorded
166
+ * execution produces durable `cancellation_unresolved` evidence and throws. Since execution
167
+ * binding accepts only running Tasks, a handle discovered after this point cannot be attached by
168
+ * retrying this helper; application-owned reconciliation must stop that handle and persist its
169
+ * cleanup evidence and measured settlement separately. If `adapter.stop` fails ambiguously, retry
170
+ * this same call—the cancelled transition and adapter stop are both expected to be idempotent.
171
+ *
172
+ * Returned final usage is recorded once. Known cost then settles the Task; unknown cost retains the
173
+ * reservation and leaves reconciliation evidence for later measurement.
174
+ *
175
+ * @param options - Exact Task attempt, cancellation reason, stores, and provider adapter.
176
+ * @returns The cancelled Task after provider usage recording and any possible settlement.
177
+ * @throws `ExecutionFenceError` when the attempt has been replaced.
178
+ * @throws When execution discovery is unresolved or the provider stop request fails.
179
+ */
72
180
  declare function cancelExecution(options: CancelExecutionOptions): Promise<Task>;
73
181
  //#endregion
74
182
  export { CancelExecutionOptions, CancelExecutionStores, ExecutionAdapter, ExecutionFenceError, ExecutionInspectionResult, InspectExecutionOptions, InspectExecutionStores, LaunchExecutionOptions, LaunchExecutionStores, RecordExecutionUsageOptions, RecordExecutionUsageStores, StartExecutionInput, cancelExecution, executionLaunchKey, inspectExecution, launchExecution, recordExecutionUsage };