@nylorun/runtime 0.17.0-beta → 0.19.0-beta

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 (223) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +88 -58
  3. package/dist/adapters/execution/restate.d.ts +1 -3
  4. package/dist/adapters/execution/restate.js +4 -31
  5. package/dist/adapters/replication/pgoutput.js +1 -1
  6. package/dist/adapters/sandbox/local.js +1 -1
  7. package/dist/adapters/sandbox/virtual.js +1 -1
  8. package/dist/api/ag-ui/routes.js +1 -1
  9. package/dist/api/ag-ui/translate.d.ts +1 -3
  10. package/dist/api/ag-ui/translate.js +2 -17
  11. package/dist/api/components.d.ts +4871 -2614
  12. package/dist/api/components.js +12 -11
  13. package/dist/api/gate/routes.d.ts +7 -8
  14. package/dist/api/gate/routes.js +58 -52
  15. package/dist/api/http/app.d.ts +6 -0
  16. package/dist/api/http/app.js +18 -14
  17. package/dist/api/http/define.d.ts +14 -2
  18. package/dist/api/http/define.js +34 -15
  19. package/dist/api/http/routes/access.d.ts +3 -3
  20. package/dist/api/http/routes/access.js +18 -17
  21. package/dist/api/http/routes/artifacts.js +2 -4
  22. package/dist/api/http/routes/files.d.ts +8 -0
  23. package/dist/api/http/routes/files.js +77 -0
  24. package/dist/api/http/routes/keys.d.ts +10 -0
  25. package/dist/api/http/routes/keys.js +64 -0
  26. package/dist/api/http/routes/me.js +5 -2
  27. package/dist/api/http/routes/reads.d.ts +66 -0
  28. package/dist/api/http/routes/reads.js +127 -0
  29. package/dist/api/http/routes/sandboxes.js +7 -4
  30. package/dist/api/http/routes/sessions.js +25 -8
  31. package/dist/api/http/routes/tenant.js +15 -18
  32. package/dist/api/http/routes/vaults.d.ts +10 -9
  33. package/dist/api/http/routes/vaults.js +27 -23
  34. package/dist/api/openapi.d.ts +10 -11
  35. package/dist/api/openapi.js +291 -61
  36. package/dist/artifacts/links.js +1 -1
  37. package/dist/core/flow-host.d.ts +44 -23
  38. package/dist/core/flow-host.js +88 -106
  39. package/dist/core/provider.d.ts +4 -1
  40. package/dist/core/provider.js +6 -1
  41. package/dist/core/sandbox-routes.d.ts +0 -9
  42. package/dist/core/sandbox-routes.js +2 -21
  43. package/dist/execution/memory.d.ts +4 -5
  44. package/dist/execution/memory.js +11 -25
  45. package/dist/execution/types.d.ts +3 -33
  46. package/dist/execution/types.js +2 -12
  47. package/dist/gates/http-tool.d.ts +86 -0
  48. package/dist/gates/http-tool.js +197 -0
  49. package/dist/gates/mcp-handler.d.ts +3 -0
  50. package/dist/gates/mcp-handler.js +1 -3
  51. package/dist/gates/meter.d.ts +2 -2
  52. package/dist/gates/meter.js +6 -0
  53. package/dist/gates/tenant-vaults.d.ts +5 -9
  54. package/dist/gates/tool-calls.d.ts +7 -15
  55. package/dist/gates/tool-calls.js +4 -4
  56. package/dist/gates/tool-client.d.ts +1 -1
  57. package/dist/gates/tool-client.js +23 -47
  58. package/dist/gates/tool-contract.d.ts +27 -39
  59. package/dist/gates/tool-contract.js +30 -21
  60. package/dist/gates/tool-gate.d.ts +25 -24
  61. package/dist/gates/tool-gate.js +11 -7
  62. package/dist/harness/calls.d.ts +10 -4
  63. package/dist/harness/calls.js +15 -6
  64. package/dist/harness/executors.d.ts +1 -1
  65. package/dist/harness/executors.js +17 -12
  66. package/dist/harness/main.d.ts +2 -2
  67. package/dist/harness/main.js +5 -13
  68. package/dist/harness/service.d.ts +4 -5
  69. package/dist/harness/service.js +9 -2
  70. package/dist/harness-api/in-process.js +2 -2
  71. package/dist/harness-api/record.d.ts +6 -3
  72. package/dist/harness-api/record.js +65 -72
  73. package/dist/harness-api/server.d.ts +1 -9
  74. package/dist/harness-api/server.js +19 -17
  75. package/dist/harness-api/start.js +1 -5
  76. package/dist/harness-api/workspace.js +2 -3
  77. package/dist/harness-api/ws-server.js +1 -1
  78. package/dist/host/app.d.ts +2 -9
  79. package/dist/host/app.js +15 -25
  80. package/dist/host/config-for.d.ts +1 -5
  81. package/dist/host/config-for.js +0 -3
  82. package/dist/host/config.d.ts +0 -5
  83. package/dist/host/create-host.d.ts +3 -33
  84. package/dist/host/create-host.js +18 -94
  85. package/dist/host/environment.d.ts +0 -3
  86. package/dist/host/environment.js +0 -9
  87. package/dist/host/gates.d.ts +4 -1
  88. package/dist/host/gates.js +3 -2
  89. package/dist/host/http.d.ts +1 -2
  90. package/dist/host/http.js +1 -7
  91. package/dist/host/index.d.ts +2 -2
  92. package/dist/host/index.js +2 -2
  93. package/dist/host/main.js +27 -34
  94. package/dist/host/operate.d.ts +29 -0
  95. package/dist/host/operate.js +223 -0
  96. package/dist/host/stack-config.d.ts +12 -12
  97. package/dist/host/stack-config.js +5 -32
  98. package/dist/index.d.ts +1 -1
  99. package/dist/index.js +1 -1
  100. package/dist/infra/workers.d.ts +1 -1
  101. package/dist/keys/keys.js +2 -2
  102. package/dist/management-openapi.json +6491 -0
  103. package/dist/mcp/connect.d.ts +8 -4
  104. package/dist/mcp/connect.js +6 -26
  105. package/dist/mcp/pool.d.ts +5 -9
  106. package/dist/mcp/pool.js +8 -19
  107. package/dist/mcp/snapshot.d.ts +8 -2
  108. package/dist/mcp/snapshot.js +29 -2
  109. package/dist/media.d.ts +1 -1
  110. package/dist/media.js +1 -1
  111. package/dist/model/pi-model.js +3 -0
  112. package/dist/openapi.json +4700 -12350
  113. package/dist/reads/access.d.ts +4 -0
  114. package/dist/reads/access.js +12 -0
  115. package/dist/reads/cursor.d.ts +11 -0
  116. package/dist/reads/cursor.js +80 -0
  117. package/dist/reads/types.d.ts +25 -0
  118. package/dist/reads/types.js +1 -0
  119. package/dist/sandbox/manager.d.ts +16 -0
  120. package/dist/sandbox/manager.js +40 -0
  121. package/dist/sandbox/session-sandbox.js +13 -3
  122. package/dist/sandbox/skills.d.ts +13 -0
  123. package/dist/sandbox/skills.js +46 -0
  124. package/dist/sandbox/types.d.ts +2 -2
  125. package/dist/store/ownership.d.ts +9 -0
  126. package/dist/store/ownership.js +13 -0
  127. package/dist/store/postgres/connect.d.ts +2 -0
  128. package/dist/store/postgres/connect.js +29 -0
  129. package/dist/store/postgres/drizzle/0011_key_roles.sql +3 -0
  130. package/dist/store/postgres/drizzle/0012_session_reads.sql +8 -0
  131. package/dist/store/postgres/drizzle/0013_definition_files.sql +16 -0
  132. package/dist/store/postgres/drizzle/0014_action_endpoints_removed.sql +2 -0
  133. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  134. package/dist/store/postgres/reads.d.ts +3 -0
  135. package/dist/store/postgres/reads.js +272 -0
  136. package/dist/store/postgres/schema.d.ts +294 -489
  137. package/dist/store/postgres/schema.js +41 -76
  138. package/dist/store/postgres/store.js +51 -154
  139. package/dist/store/postgres/tenant.d.ts +34 -2
  140. package/dist/store/postgres/tenant.js +16 -32
  141. package/dist/store/types.d.ts +27 -96
  142. package/dist/store/types.js +0 -1
  143. package/dist/streams/control.d.ts +0 -2
  144. package/dist/streams/control.js +0 -5
  145. package/dist/streams/types.d.ts +1 -10
  146. package/dist/tenant/advance.d.ts +3 -3
  147. package/dist/tenant/advance.js +10 -7
  148. package/dist/tenant/auth.d.ts +14 -9
  149. package/dist/tenant/auth.js +24 -28
  150. package/dist/tenant/cause.d.ts +3 -3
  151. package/dist/tenant/cause.js +1 -1
  152. package/dist/tenant/commands.d.ts +2 -18
  153. package/dist/tenant/commands.js +35 -172
  154. package/dist/tenant/context.d.ts +14 -24
  155. package/dist/tenant/definition-files.d.ts +24 -0
  156. package/dist/tenant/definition-files.js +103 -0
  157. package/dist/tenant/effects.d.ts +18 -20
  158. package/dist/tenant/effects.js +56 -119
  159. package/dist/tenant/ephemeral.d.ts +7 -6
  160. package/dist/tenant/ephemeral.js +3 -11
  161. package/dist/tenant/host-token.js +1 -1
  162. package/dist/tenant/jwt.js +1 -1
  163. package/dist/tenant/module.d.ts +1 -4
  164. package/dist/tenant/module.js +3 -12
  165. package/dist/tenant/operator-keys.d.ts +34 -8
  166. package/dist/tenant/operator-keys.js +50 -18
  167. package/dist/tenant/outbound.d.ts +10 -1
  168. package/dist/tenant/outbound.js +44 -27
  169. package/dist/tenant/paths.d.ts +1 -1
  170. package/dist/tenant/paths.js +1 -3
  171. package/dist/tenant/principals.d.ts +11 -2
  172. package/dist/tenant/principals.js +27 -8
  173. package/dist/tenant/run-token.js +3 -3
  174. package/dist/tenant/runtime.d.ts +4 -6
  175. package/dist/tenant/runtime.js +19 -18
  176. package/dist/tenant/sandboxes.d.ts +4 -1
  177. package/dist/tenant/sandboxes.js +11 -0
  178. package/dist/tenant/scheduler.d.ts +1 -5
  179. package/dist/tenant/scheduler.js +2 -5
  180. package/dist/tenant/session-streams.d.ts +2 -1
  181. package/dist/tenant/session-streams.js +39 -5
  182. package/dist/tenant/session.d.ts +8 -41
  183. package/dist/tenant/session.js +0 -37
  184. package/dist/tenant/sessions.js +16 -8
  185. package/dist/tenant/signing-keys.d.ts +3 -3
  186. package/dist/tenant/signing-keys.js +5 -5
  187. package/dist/tenant/skill-tool.d.ts +20 -0
  188. package/dist/tenant/skill-tool.js +72 -0
  189. package/dist/tenant/slim.d.ts +1 -1
  190. package/dist/tenant/slim.js +1 -1
  191. package/dist/tenant/status.js +7 -12
  192. package/dist/tenant/store-pg.d.ts +15 -0
  193. package/dist/tenant/store-pg.js +7 -3
  194. package/dist/tenant/streams.d.ts +3 -10
  195. package/dist/tenant/streams.js +1 -24
  196. package/dist/tenant/sweep.js +0 -5
  197. package/dist/tenant/transcript.d.ts +2 -2
  198. package/dist/tenant/transcript.js +2 -2
  199. package/dist/tenant/types.d.ts +15 -22
  200. package/dist/tenant/worker.d.ts +1 -3
  201. package/dist/tenant/worker.js +3 -8
  202. package/dist/vault/sources.d.ts +7 -1
  203. package/dist/vault/sources.js +22 -11
  204. package/dist/version.d.ts +1 -1
  205. package/dist/version.js +1 -1
  206. package/package.json +4 -4
  207. package/dist/admin-openapi.json +0 -904
  208. package/dist/api/http/routes/actions.d.ts +0 -8
  209. package/dist/api/http/routes/actions.js +0 -75
  210. package/dist/api/http/routes/endpoints.d.ts +0 -7
  211. package/dist/api/http/routes/endpoints.js +0 -100
  212. package/dist/host/admin-api.d.ts +0 -30
  213. package/dist/host/admin-api.js +0 -140
  214. package/dist/mcp/stdio.d.ts +0 -17
  215. package/dist/mcp/stdio.js +0 -82
  216. package/dist/tenant/actions.d.ts +0 -14
  217. package/dist/tenant/actions.js +0 -29
  218. package/dist/tenant/delivery-token.d.ts +0 -41
  219. package/dist/tenant/delivery-token.js +0 -104
  220. package/dist/tenant/delivery.d.ts +0 -62
  221. package/dist/tenant/delivery.js +0 -481
  222. package/dist/tenant/endpoints.d.ts +0 -67
  223. package/dist/tenant/endpoints.js +0 -90
@@ -1,5 +1,5 @@
1
- import type { ActionOutcome } from "@nylorun/core/contracts";
2
- import type { JsonValue, WorkflowManifest } from "@nylorun/core/define";
1
+ import type { EffectOutcome } from "@nylorun/core/contracts";
2
+ import { type JsonValue, type WorkflowManifest } from "@nylorun/core/define";
3
3
  import type { HostEffect } from "@nylorun/harness/run";
4
4
  import type { Wake } from "../execution/types.js";
5
5
  import type { EffectDoc, Tx } from "../store/types.js";
@@ -20,6 +20,31 @@ export declare function isWorkflowManifest(manifest: unknown): manifest is Workf
20
20
  /** Flow tool-node effect: `tool` with path/key and no agent capabilityId. */
21
21
  export declare function isFlowToolEffect(request: HostEffect): boolean;
22
22
  export declare function isFlowEffect(request: HostEffect): boolean;
23
+ /**
24
+ * What a flow `agent` effect sends its linked session (D12): the stage's input, and, when the
25
+ * engine sent the flow's own input beside it, that input first as the original request. Pure,
26
+ * so a replayed effect sends the same message.
27
+ */
28
+ export declare function linkedMessageInput(body: {
29
+ readonly input: JsonValue;
30
+ readonly flowInput?: JsonValue;
31
+ }): JsonValue;
32
+ /**
33
+ * Settle a flow `agent` effect with its linked turn's outcome, in the caller's transaction,
34
+ * which holds the workflow session's lock. A verifier agent's verdict is recorded as
35
+ * `loop.verified`; an outcome that is not a verdict fails the Loop in the engine instead.
36
+ */
37
+ export declare function settleAgentEffect(input: {
38
+ readonly t: Tx;
39
+ readonly effect: FlowEffect;
40
+ readonly outcome: EffectOutcome;
41
+ }): Promise<void>;
42
+ /**
43
+ * Record a Loop verifier's verdict as `loop.verified`: a verifier agent's output, or an HTTP
44
+ * verifier's answer (`role: "verify-http"`, a completed HTTP outcome). Anything that is not a
45
+ * verdict records nothing; the engine fails the Loop on it.
46
+ */
47
+ export declare function recordVerdict(t: Tx, request: HostEffect, value: unknown): Promise<void>;
23
48
  export type FlowLink = {
24
49
  readonly workflowSessionId: string;
25
50
  readonly path: string;
@@ -60,30 +85,28 @@ export type FlowWait = {
60
85
  readonly path: string;
61
86
  readonly interactionId: string;
62
87
  readonly kind: string;
88
+ /** The interaction as asked (`prompt`, `metadata`), when the wait records it. */
89
+ readonly interaction?: unknown;
63
90
  readonly invocationId?: string;
64
91
  readonly wait?: unknown;
65
92
  readonly status?: string;
66
93
  };
67
94
  export type CancelSiblingResult = {
68
- readonly cancelledActions: string[];
69
- readonly uncertainActions: string[];
70
95
  readonly agentSessionIds: string[];
71
96
  };
72
97
  export type CascadeCancelPlan = {
73
98
  /** Linked agent sessions to cancel, deepest path first. */
74
99
  readonly agentSessionIds: string[];
75
- readonly pendingActionIds: string[];
76
- readonly deliveringActionIds: string[];
77
100
  };
78
101
  /** Wakes a session after commit (`DurableExecution.wake` through the Tenant context). */
79
102
  type Schedule = (sessionId: string, wake: Wake) => void | Promise<void>;
80
103
  /** Effect documents as the flow host writes them. */
81
104
  export type FlowEffect = EffectDoc & {
82
105
  agentSessionId?: string;
83
- outcome?: ActionOutcome;
106
+ outcome?: EffectOutcome;
84
107
  error?: string;
85
108
  };
86
- /** Active agent turns + pending/delivering actions for one workflow turn. */
109
+ /** Active agent turns + tool nodes in flight for one workflow turn. */
87
110
  export declare function countActiveFlowWork(t: Tx, workflowSessionId: string, turnId: string): Promise<number>;
88
111
  /** The `commands` document key of a session command's idempotency key (`tenant/commands.ts`). */
89
112
  export declare function commandKey(sessionId: string, idempotencyKey: string): string;
@@ -134,10 +157,7 @@ export declare function reconcilePendingAgentEffect(input: {
134
157
  }): Promise<boolean>;
135
158
  /** Path depth for deepest-first cancel ordering. */
136
159
  export declare function pathDepth(path: string): number;
137
- /**
138
- * Plan a cancel cascade for a workflow session: linked agents deepest first,
139
- * then pending → cancelled and delivering → uncertain actions (SD-P11 / WF-R53).
140
- */
160
+ /** Plan a cancel cascade for a workflow session: linked agents deepest first (SD-P11 / WF-R53). */
141
161
  export declare function planCancelCascade(input: {
142
162
  readonly t: Tx;
143
163
  readonly workflowSessionId: string;
@@ -145,8 +165,7 @@ export declare function planCancelCascade(input: {
145
165
  }): Promise<CascadeCancelPlan>;
146
166
  /**
147
167
  * Cancel sibling work under a Parallel/Map parent when one branch fails (PAR-R6).
148
- * Agent turns listed in `agentSessionIds` are returned for the caller to cancel;
149
- * pending actions → cancelled, delivering tool actions → uncertain.
168
+ * Agent turns listed in `agentSessionIds` are returned for the caller to cancel.
150
169
  * When `cancelEffectIds` is provided, those effects are marked cancelled and their
151
170
  * paths are included in the sibling path set.
152
171
  */
@@ -159,18 +178,12 @@ export declare function cancelSiblingWork(input: {
159
178
  /** Effect ids the flow engine marked for fail-fast cancel. */
160
179
  readonly cancelEffectIds?: readonly string[];
161
180
  }): Promise<CancelSiblingResult>;
162
- /**
163
- * Apply cancel fencing to workflow-session actions (pending→cancelled,
164
- * delivering→uncertain).
165
- */
166
- export declare function fenceWorkflowActions(input: {
181
+ /** Cancel a cancelled workflow turn's queued effects, which never started. */
182
+ export declare function cancelQueuedEffects(input: {
167
183
  readonly t: Tx;
168
184
  readonly workflowSessionId: string;
169
185
  readonly turnId: string | null;
170
- }): Promise<{
171
- cancelled: string[];
172
- uncertain: string[];
173
- }>;
186
+ }): Promise<string[]>;
174
187
  /**
175
188
  * Aggregate human waits across the workflow session and linked agent sessions (WF-R51).
176
189
  */
@@ -178,6 +191,14 @@ export declare function aggregateWaits(input: {
178
191
  readonly t: Tx;
179
192
  readonly workflowSessionId: string;
180
193
  }): Promise<FlowWait[]>;
194
+ /**
195
+ * The workflow-owned interaction `interactionId` among a workflow's `waits` (a tool node that
196
+ * asked), or undefined.
197
+ */
198
+ export declare function flowInteractionOf(raw: unknown, workflowSessionId: string, interactionId: string): {
199
+ readonly id: string;
200
+ readonly kind: string;
201
+ } | undefined;
181
202
  /**
182
203
  * Look up which session owns an interaction id. Used for 409 foreign approvals (WF-R52).
183
204
  */
@@ -14,6 +14,9 @@
14
14
  * - `deriveAgentEffectSessionId(workflowSessionId, path, request): string`
15
15
  * - `isWorkflowManifest(manifest): manifest is WorkflowManifest`
16
16
  * - `isFlowToolEffect(request): boolean`, `isFlowEffect(request): boolean`
17
+ * - `linkedMessageInput(body): JsonValue`
18
+ * - `settleAgentEffect({ t, effect, outcome }): Promise<void>`
19
+ * - `recordVerdict(t, request, value): Promise<void>`
17
20
  * - `pathDepth(path): number`
18
21
  * - `countActiveFlowWork(t, workflowSessionId, turnId): Promise<number>`
19
22
  * - `commandKey(sessionId, idempotencyKey): string`
@@ -25,8 +28,9 @@
25
28
  * - `reconcilePendingAgentEffect({ t, effectId, schedule }): Promise<boolean>`
26
29
  * - `planCancelCascade({ t, workflowSessionId, turnId }): Promise<CascadeCancelPlan>`
27
30
  * - `cancelSiblingWork({ t, workflowSessionId, turnId, siblingPaths?, cancelEffectIds? }): Promise<CancelSiblingResult>`
28
- * - `fenceWorkflowActions({ t, workflowSessionId, turnId }): Promise<{ cancelled; uncertain }>`
31
+ * - `cancelQueuedEffects({ t, workflowSessionId, turnId }): Promise<string[]>`
29
32
  * - `aggregateWaits({ t, workflowSessionId }): Promise<FlowWait[]>`
33
+ * - `flowInteractionOf(waits, workflowSessionId, interactionId): { id; kind } | undefined`
30
34
  * - `findInteractionOwner({ t, workflowSessionId, interactionId }): Promise<{ sessionId; path } | undefined>`
31
35
  * - `foreignInteractionConflict({ t, workflowSessionId, interactionId }): Promise<{ status: 409; message; ownerSessionId } | undefined>`
32
36
  * - `wakeForQueuedEffects({ t, workflowSessionId, turnId, limits, schedule }): Promise<boolean>`
@@ -40,10 +44,11 @@
40
44
  * commits. The effect is bound to its turn through the message's idempotency key
41
45
  * (`linkedMessageKey`): the `commands` document of that key names the turn it opened.
42
46
  *
43
- * The Tenant sweep (`tenant/sweep.ts`) calls the reconcile and re-offer functions, one
44
- * transaction per effect or Action.
47
+ * The Tenant sweep (`tenant/sweep.ts`) calls the reconcile functions, one transaction per
48
+ * effect.
45
49
  */
46
50
  import { createHash } from "node:crypto";
51
+ import { isVerdict } from "@nylorun/core/define";
47
52
  import { mayDispatchMore } from "./limits.js";
48
53
  /** Deterministic agent session id: derive(workflowSessionId, path, …parts). */
49
54
  export function deriveSessionId(workflowSessionId, path, ...parts) {
@@ -84,23 +89,61 @@ export function isFlowToolEffect(request) {
84
89
  request.capabilityId === undefined);
85
90
  }
86
91
  export function isFlowEffect(request) {
87
- return (request.kind === "agent" ||
88
- request.kind === "fn" ||
89
- request.kind === "verify" ||
90
- isFlowToolEffect(request));
92
+ return request.kind === "agent" || isFlowToolEffect(request);
93
+ }
94
+ /** Pretty JSON for a message, or a string as it is. */
95
+ function text(value) {
96
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
97
+ }
98
+ /**
99
+ * What a flow `agent` effect sends its linked session (D12): the stage's input, and, when the
100
+ * engine sent the flow's own input beside it, that input first as the original request. Pure,
101
+ * so a replayed effect sends the same message.
102
+ */
103
+ export function linkedMessageInput(body) {
104
+ if (body.flowInput === undefined)
105
+ return body.input;
106
+ return `Original request:\n${text(body.flowInput)}\n\n${text(body.input)}`;
107
+ }
108
+ /**
109
+ * Settle a flow `agent` effect with its linked turn's outcome, in the caller's transaction,
110
+ * which holds the workflow session's lock. A verifier agent's verdict is recorded as
111
+ * `loop.verified`; an outcome that is not a verdict fails the Loop in the engine instead.
112
+ */
113
+ export async function settleAgentEffect(input) {
114
+ const { t, effect, outcome } = input;
115
+ await t.put("effects", effect.request.effectId, { ...effect, status: "completed", outcome });
116
+ if (effect.request.context?.role === "verify-agent")
117
+ await recordVerdict(t, effect.request, outcome.value);
118
+ }
119
+ /**
120
+ * Record a Loop verifier's verdict as `loop.verified`: a verifier agent's output, or an HTTP
121
+ * verifier's answer (`role: "verify-http"`, a completed HTTP outcome). Anything that is not a
122
+ * verdict records nothing; the engine fails the Loop on it.
123
+ */
124
+ export async function recordVerdict(t, request, value) {
125
+ const answer = value;
126
+ const verdict = request.context?.role !== "verify-http"
127
+ ? value
128
+ : answer?.kind === "completed"
129
+ ? answer.output
130
+ : undefined;
131
+ if (!isVerdict(verdict))
132
+ return;
133
+ await t.event(request.sessionId, request.turnId, "loop.verified", {
134
+ path: String(request.context.loopPath ?? request.path ?? ""),
135
+ n: Number(request.context.n ?? 1),
136
+ pass: verdict.pass,
137
+ ...(verdict.pass ? {} : { feedback: verdict.feedback }),
138
+ ...(verdict.data !== undefined ? { data: verdict.data } : {}),
139
+ });
91
140
  }
92
- /** Actions still to be settled: waiting to be delivered, or being delivered. */
93
- const OPEN_ACTION = ["pending", "delivering"];
94
141
  function scheduleAfterCommit(t, schedule, id, wake) {
95
142
  t.afterCommit(() => schedule(id, wake));
96
143
  }
97
- /** Active agent turns + pending/delivering actions for one workflow turn. */
144
+ /** Active agent turns + tool nodes in flight for one workflow turn. */
98
145
  export async function countActiveFlowWork(t, workflowSessionId, turnId) {
99
146
  const effects = await t.effectsForTurn(workflowSessionId, turnId);
100
- const openActions = new Set((await t.actionsForSession(workflowSessionId, {
101
- turnId,
102
- statuses: OPEN_ACTION,
103
- })).map((action) => action.actionId));
104
147
  let n = 0;
105
148
  for (const effect of effects) {
106
149
  if (effect.status === "queued")
@@ -111,10 +154,7 @@ export async function countActiveFlowWork(t, workflowSessionId, turnId) {
111
154
  n += 1;
112
155
  continue;
113
156
  }
114
- if ((effect.request?.kind === "fn" ||
115
- effect.request?.kind === "verify" ||
116
- isFlowToolEffect(effect.request)) &&
117
- openActions.has(effect.request.effectId))
157
+ if (isFlowToolEffect(effect.request) && effect.status === "invoking")
118
158
  n += 1;
119
159
  }
120
160
  return n;
@@ -199,11 +239,7 @@ export async function wakeLinkedWorkflow(input) {
199
239
  },
200
240
  }
201
241
  : { value: input.output ?? null };
202
- await t.put("effects", link.effectId, {
203
- ...effect,
204
- status: "completed",
205
- outcome,
206
- });
242
+ await settleAgentEffect({ t, effect, outcome });
207
243
  workflow.status = "runnable";
208
244
  await t.put("sessions", workflow.id, workflow);
209
245
  scheduleAfterCommit(t, input.schedule, workflow.id, {
@@ -259,45 +295,22 @@ export function pathDepth(path) {
259
295
  return 0;
260
296
  return path.split("/").filter(Boolean).length;
261
297
  }
262
- /**
263
- * Plan a cancel cascade for a workflow session: linked agents deepest first,
264
- * then pending → cancelled and delivering → uncertain actions (SD-P11 / WF-R53).
265
- */
298
+ /** Plan a cancel cascade for a workflow session: linked agents deepest first (SD-P11 / WF-R53). */
266
299
  export async function planCancelCascade(input) {
267
300
  const { t } = input;
268
301
  const agentEntries = (await t.linkedSessions(input.workflowSessionId)).map((linked) => ({ sessionId: linked.agentSessionId, path: linked.link.path }));
269
302
  agentEntries.sort((a, b) => pathDepth(b.path) - pathDepth(a.path));
270
- const pendingActionIds = [];
271
- const deliveringActionIds = [];
272
- const actions = await t.actionsForSession(input.workflowSessionId, {
273
- ...(input.turnId !== null ? { turnId: input.turnId } : {}),
274
- statuses: OPEN_ACTION,
275
- });
276
- for (const action of actions) {
277
- if (action.status === "pending")
278
- pendingActionIds.push(action.actionId);
279
- // Being delivered: the code may already be running.
280
- else
281
- deliveringActionIds.push(action.actionId);
282
- }
283
- return {
284
- agentSessionIds: agentEntries.map((e) => e.sessionId),
285
- pendingActionIds,
286
- deliveringActionIds,
287
- };
303
+ return { agentSessionIds: agentEntries.map((e) => e.sessionId) };
288
304
  }
289
305
  /**
290
306
  * Cancel sibling work under a Parallel/Map parent when one branch fails (PAR-R6).
291
- * Agent turns listed in `agentSessionIds` are returned for the caller to cancel;
292
- * pending actions → cancelled, delivering tool actions → uncertain.
307
+ * Agent turns listed in `agentSessionIds` are returned for the caller to cancel.
293
308
  * When `cancelEffectIds` is provided, those effects are marked cancelled and their
294
309
  * paths are included in the sibling path set.
295
310
  */
296
311
  export async function cancelSiblingWork(input) {
297
312
  const { t } = input;
298
313
  await t.lockSession(input.workflowSessionId);
299
- const cancelledActions = [];
300
- const uncertainActions = [];
301
314
  const agentSessionIds = [];
302
315
  const siblingPaths = new Set(input.siblingPaths ?? []);
303
316
  for (const effectId of input.cancelEffectIds ?? []) {
@@ -328,66 +341,21 @@ export async function cancelSiblingWork(input) {
328
341
  ["running", "runnable", "paused", "waiting"].includes(session.status))
329
342
  agentSessionIds.push(agentSessionId);
330
343
  }
331
- const actions = await t.actionsForSession(input.workflowSessionId, {
332
- turnId: input.turnId,
333
- statuses: OPEN_ACTION,
334
- });
335
- for (const action of actions) {
336
- if (!matchesSibling(action.path))
337
- continue;
338
- // Delivered work may already have had an external effect.
339
- const next = action.status === "pending" ? "cancelled" : "uncertain";
340
- action.status = next;
341
- await t.put("actions", action.actionId, action);
342
- const effect = await t.get("effects", action.actionId);
343
- if (effect) {
344
- effect.status = next;
345
- await t.put("effects", action.actionId, effect);
346
- }
347
- if (next === "cancelled")
348
- cancelledActions.push(action.actionId);
349
- else
350
- uncertainActions.push(action.actionId);
351
- }
352
- return { cancelledActions, uncertainActions, agentSessionIds };
344
+ return { agentSessionIds };
353
345
  }
354
- /**
355
- * Apply cancel fencing to workflow-session actions (pending→cancelled,
356
- * delivering→uncertain).
357
- */
358
- export async function fenceWorkflowActions(input) {
346
+ /** Cancel a cancelled workflow turn's queued effects, which never started. */
347
+ export async function cancelQueuedEffects(input) {
359
348
  const { t } = input;
360
349
  await t.lockSession(input.workflowSessionId);
361
350
  const turn = input.turnId !== null ? { turnId: input.turnId } : {};
362
351
  const cancelled = [];
363
- const uncertain = [];
364
- const actions = await t.actionsForSession(input.workflowSessionId, {
365
- ...turn,
366
- statuses: OPEN_ACTION,
367
- });
368
- for (const action of actions) {
369
- // Delivered work may already have had an external effect.
370
- const next = action.status === "pending" ? "cancelled" : "uncertain";
371
- action.status = next;
372
- await t.put("actions", action.actionId, action);
373
- const effect = await t.get("effects", action.actionId);
374
- if (effect) {
375
- effect.status = next;
376
- await t.put("effects", action.actionId, effect);
377
- }
378
- if (next === "cancelled")
379
- cancelled.push(action.actionId);
380
- else
381
- uncertain.push(action.actionId);
382
- }
383
- // Drop queued effects that never started.
384
352
  const queued = await t.effectsForSession(input.workflowSessionId, { ...turn, statuses: ["queued"] });
385
353
  for (const effect of queued) {
386
354
  effect.status = "cancelled";
387
355
  await t.put("effects", effect.request.effectId, effect);
388
356
  cancelled.push(effect.request.effectId);
389
357
  }
390
- return { cancelled, uncertain };
358
+ return cancelled;
391
359
  }
392
360
  function waitsFromSession(sessionId, path, raw) {
393
361
  if (!Array.isArray(raw))
@@ -400,9 +368,10 @@ function waitsFromSession(sessionId, path, raw) {
400
368
  continue;
401
369
  waits.push({
402
370
  sessionId,
403
- path,
371
+ path: typeof call.path === "string" ? call.path : path,
404
372
  interactionId,
405
373
  kind: String(interaction?.kind ?? call.kind ?? "approval"),
374
+ ...(call.interaction !== undefined ? { interaction: call.interaction } : {}),
406
375
  ...(call.invocationId !== undefined
407
376
  ? { invocationId: String(call.invocationId) }
408
377
  : {}),
@@ -421,13 +390,9 @@ export async function aggregateWaits(input) {
421
390
  if (!workflow)
422
391
  return [];
423
392
  const waits = [];
424
- // Workflow-owned interactions (tool-node / verify approvals) live on the workflow session.
425
- if (Array.isArray(workflow.waits))
426
- waits.push(...waitsFromSession(workflow.id, "", workflow.waits));
427
- else if (workflow.waits &&
428
- typeof workflow.waits === "object" &&
429
- Array.isArray(workflow.waits.interactions))
430
- waits.push(...waitsFromSession(workflow.id, "", workflow.waits.interactions));
393
+ // Workflow-owned interactions (tool-node approvals) live on the workflow session. Waits it
394
+ // copied from linked sessions are read from those sessions below, never from this copy.
395
+ waits.push(...waitsFromSession(workflow.id, "", ownWaits(workflow.waits, workflow.id)));
431
396
  for (const { agentSessionId, link, session } of await t.linkedSessions(input.workflowSessionId)) {
432
397
  if (session.status !== "paused")
433
398
  continue;
@@ -435,6 +400,23 @@ export async function aggregateWaits(input) {
435
400
  }
436
401
  return waits;
437
402
  }
403
+ /** The entries of a workflow's `waits` that the workflow itself owns. */
404
+ function ownWaits(raw, workflowSessionId) {
405
+ const list = Array.isArray(raw)
406
+ ? raw
407
+ : raw && typeof raw === "object" && Array.isArray(raw.interactions)
408
+ ? raw.interactions
409
+ : [];
410
+ return list.filter((call) => call?.sessionId === undefined || call.sessionId === workflowSessionId);
411
+ }
412
+ /**
413
+ * The workflow-owned interaction `interactionId` among a workflow's `waits` (a tool node that
414
+ * asked), or undefined.
415
+ */
416
+ export function flowInteractionOf(raw, workflowSessionId, interactionId) {
417
+ const hit = waitsFromSession(workflowSessionId, "", ownWaits(raw, workflowSessionId)).find((wait) => wait.interactionId === interactionId);
418
+ return hit && { id: hit.interactionId, kind: hit.kind };
419
+ }
438
420
  /**
439
421
  * Look up which session owns an interaction id. Used for 409 foreign approvals (WF-R52).
440
422
  */
@@ -8,5 +8,8 @@ export declare function gatewayModel(options: {
8
8
  token: string;
9
9
  model: string;
10
10
  }): ModelProvider;
11
- /** Deterministic release fixture; still uses the real engine and remote customer tool. */
11
+ /**
12
+ * Deterministic release fixture; still uses the real engine. With a `lookup_order` tool it looks
13
+ * up the demo order through it once; without one it answers in text.
14
+ */
12
15
  export declare function toolFixtureModel(): ModelProvider;
@@ -22,7 +22,10 @@ export function gatewayModel(options) {
22
22
  return response.json();
23
23
  };
24
24
  }
25
- /** Deterministic release fixture; still uses the real engine and remote customer tool. */
25
+ /**
26
+ * Deterministic release fixture; still uses the real engine. With a `lookup_order` tool it looks
27
+ * up the demo order through it once; without one it answers in text.
28
+ */
26
29
  export function toolFixtureModel() {
27
30
  return async (effect) => {
28
31
  const call = effect.input;
@@ -46,6 +49,8 @@ export function toolFixtureModel() {
46
49
  },
47
50
  ],
48
51
  };
52
+ if (!call.tools.some((tool) => tool.name === "lookup_order"))
53
+ return { output: [{ type: "text", text: "Hello from the fixture model." }] };
49
54
  return {
50
55
  output: [
51
56
  {
@@ -1,4 +1,3 @@
1
- import type { Action } from "@nylorun/core/contracts";
2
1
  import type { WorkspacePort } from "../harness-api/workspace.js";
3
2
  import type { SandboxToolOutcome } from "../sandbox/tools.js";
4
3
  import { type SessionSandboxRef } from "../sandbox/share.js";
@@ -19,20 +18,12 @@ export type SandboxRouteDeps = {
19
18
  readonly sandbox: Pick<WorkspacePort, "run">;
20
19
  /** The session (rejects when missing) and its owner chain, read in one transaction. */
21
20
  readonly session: (id: string) => Promise<SandboxRouteRead>;
22
- readonly getAction: (id: string) => Promise<Action | undefined>;
23
21
  };
24
22
  /**
25
23
  * `POST /v1/sessions/:id/sandbox/:tool` — application principal.
26
24
  * Refused while the session has an active agent turn.
27
25
  */
28
26
  export declare function handleSessionSandboxTool(deps: SandboxRouteDeps, sessionId: string, toolName: string, body: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
29
- /**
30
- * `POST /v1/actions/:id/sandbox/:tool` — the Action endpoint, authorized by the delivery token
31
- * of the Action's current delivery (`delivery.generation`). The body is the tool's input.
32
- */
33
- export declare function handleActionSandboxTool(deps: SandboxRouteDeps, actionId: string, toolName: string, body: unknown, signal: AbortSignal, delivery: {
34
- generation: number;
35
- }): Promise<SandboxToolOutcome>;
36
27
  /**
37
28
  * Validate PutSession.sandbox attach: same owner, and a sandbox to share. A session whose
38
29
  * definition declares a sandbox must declare the owner's spec; one whose definition declares
@@ -1,6 +1,6 @@
1
1
  /**
2
- * HTTP handlers for session-scoped and claim-scoped sandbox tool endpoints.
3
- * Wired from TenantRuntime; keeps route bodies out of the hot runtime file.
2
+ * HTTP handlers for the session-scoped sandbox tool route (`POST /v1/sessions/:id/sandbox/:tool`)
3
+ * and the sandbox attach check of `PUT /v1/sessions/:id`.
4
4
  */
5
5
  import { isSandboxToolName, } from "@nylorun/core/define";
6
6
  import { capabilityForSandbox, owningSandboxSessionId, sandboxSpecOf, sandboxWorkspaceOf, sandboxSpecsEqual, sessionSandboxSpec, } from "../sandbox/share.js";
@@ -56,25 +56,6 @@ export async function handleSessionSandboxTool(deps, sessionId, toolName, body,
56
56
  const tool = parseTool(toolName);
57
57
  return runTool(deps, read, tool, toolInput(body), signal);
58
58
  }
59
- /**
60
- * `POST /v1/actions/:id/sandbox/:tool` — the Action endpoint, authorized by the delivery token
61
- * of the Action's current delivery (`delivery.generation`). The body is the tool's input.
62
- */
63
- export async function handleActionSandboxTool(deps, actionId, toolName, body, signal, delivery) {
64
- if (!body || typeof body !== "object" || Array.isArray(body))
65
- throw new SandboxRouteError(400, "Sandbox tool body must be an object");
66
- const action = await deps.getAction(actionId);
67
- if (!action)
68
- throw new SandboxRouteError(404, "Action not found");
69
- if (action.status !== "delivering" ||
70
- action.generation !== delivery.generation ||
71
- Date.parse(action.deadlineAt ?? "") <= Date.now())
72
- throw new SandboxRouteError(409, "The delivery was cancelled, lost or delivered again");
73
- const read = await deps.session(action.sessionId);
74
- if (read.session.activeTurnId !== action.turnId)
75
- throw new SandboxRouteError(409, "Action unavailable");
76
- return runTool(deps, read, parseTool(toolName), toolInput(body), signal);
77
- }
78
59
  /**
79
60
  * Validate PutSession.sandbox attach: same owner, and a sandbox to share. A session whose
80
61
  * definition declares a sandbox must declare the owner's spec; one whose definition declares
@@ -2,7 +2,7 @@ import { type DurableExecution, type SandboxSignal, type Wake, type WorkerHandle
2
2
  export interface MemoryExecutionOptions {
3
3
  /** Delay between sweep passes of one Tenant. Default 5000. */
4
4
  sweepIntervalMs?: number;
5
- /** First retry delay after `advance` or `deliver` throws; doubles per attempt. Default 50. */
5
+ /** First retry delay after `advance` throws; doubles per attempt. Default 50. */
6
6
  retryDelayMs?: number;
7
7
  /** Attempts before giving up on a throwing handler and reporting it. Default 5. */
8
8
  maxAttempts?: number;
@@ -39,7 +39,6 @@ export declare class MemoryExecution implements DurableExecution {
39
39
  private readonly onError;
40
40
  constructor(options?: MemoryExecutionOptions);
41
41
  wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
42
- deliver(tenantId: string, actionId: string): Promise<void>;
43
42
  sandbox(tenantId: string, sandboxId: string, signal: SandboxSignal): Promise<void>;
44
43
  private armSandbox;
45
44
  private runSandbox;
@@ -49,8 +48,8 @@ export declare class MemoryExecution implements DurableExecution {
49
48
  start(handlers: WorkerHandlers): Promise<void>;
50
49
  stop(): Promise<void>;
51
50
  /**
52
- * Resolves once no advance or delivery is running or queued and no busy or
53
- * retry re-run is pending. Timers and sweeps are not waited for. Test helper.
51
+ * Resolves once no advance is running or queued and no busy or retry re-run is
52
+ * pending. Timers and sweeps are not waited for. Test helper.
54
53
  */
55
54
  idle(): Promise<void>;
56
55
  private enqueue;
@@ -58,7 +57,7 @@ export declare class MemoryExecution implements DurableExecution {
58
57
  /** Drops dedupe keys older than the retention window. */
59
58
  private forgetDedupeKeys;
60
59
  private runOnce;
61
- /** Runs the key's handler; resolves to a delay when the key must run again. */
60
+ /** Runs the session's advance; resolves to a delay when it must run again. */
62
61
  private call;
63
62
  private armTimer;
64
63
  private scheduleSweep;
@@ -44,10 +44,7 @@ export class MemoryExecution {
44
44
  return;
45
45
  this.seen.set(dedupe, now);
46
46
  }
47
- this.enqueue("advance", tenantId, sessionId);
48
- }
49
- async deliver(tenantId, actionId) {
50
- this.enqueue("deliver", tenantId, actionId);
47
+ this.enqueue(tenantId, sessionId);
51
48
  }
52
49
  async sandbox(tenantId, sandboxId, signal) {
53
50
  const key = `${tenantId}\u0000${sandboxId}`;
@@ -173,8 +170,8 @@ export class MemoryExecution {
173
170
  await settleWithin(this.inflight, this.stopGraceMs);
174
171
  }
175
172
  /**
176
- * Resolves once no advance or delivery is running or queued and no busy or
177
- * retry re-run is pending. Timers and sweeps are not waited for. Test helper.
173
+ * Resolves once no advance is running or queued and no busy or retry re-run is
174
+ * pending. Timers and sweeps are not waited for. Test helper.
178
175
  */
179
176
  async idle() {
180
177
  while (this.delayed.size > 0 ||
@@ -182,14 +179,13 @@ export class MemoryExecution {
182
179
  [...this.keys.values()].some((s) => s.running || (s.queued && this.handlers)))
183
180
  await new Promise((resolve) => setTimeout(resolve, 1));
184
181
  }
185
- enqueue(kind, tenantId, id) {
186
- const key = stateKey(kind, tenantId, id);
182
+ enqueue(tenantId, sessionId) {
183
+ const key = sessionKey(tenantId, sessionId);
187
184
  let state = this.keys.get(key);
188
185
  if (!state)
189
186
  this.keys.set(key, (state = {
190
- kind,
191
187
  tenantId,
192
- id,
188
+ sessionId,
193
189
  running: false,
194
190
  queued: false,
195
191
  attempts: 0,
@@ -211,7 +207,7 @@ export class MemoryExecution {
211
207
  state.running = false;
212
208
  // Idle keys hold nothing (a retry keeps its attempt count); forget them so
213
209
  // long-lived processes stay small.
214
- const key = stateKey(state.kind, state.tenantId, state.id);
210
+ const key = sessionKey(state.tenantId, state.sessionId);
215
211
  if (!state.queued && state.attempts === 0 && this.keys.get(key) === state)
216
212
  this.keys.delete(key);
217
213
  }
@@ -228,7 +224,7 @@ export class MemoryExecution {
228
224
  async runOnce(state, handlers) {
229
225
  const controller = new AbortController();
230
226
  state.controller = controller;
231
- const again = () => this.enqueue(state.kind, state.tenantId, state.id);
227
+ const again = () => this.enqueue(state.tenantId, state.sessionId);
232
228
  try {
233
229
  const retryAfterMs = await this.call(state, handlers, controller.signal);
234
230
  state.attempts = 0;
@@ -248,16 +244,10 @@ export class MemoryExecution {
248
244
  state.controller = undefined;
249
245
  }
250
246
  }
251
- /** Runs the key's handler; resolves to a delay when the key must run again. */
247
+ /** Runs the session's advance; resolves to a delay when it must run again. */
252
248
  async call(state, handlers, signal) {
253
- if (state.kind === "advance") {
254
- const result = await handlers.advance(state.tenantId, state.id, signal);
255
- return result.status === "busy" ? result.retryAfterMs : undefined;
256
- }
257
- if (!handlers.deliver)
258
- throw new Error("WorkerHandlers.deliver is required for deliveries");
259
- const result = await handlers.deliver(state.tenantId, state.id, signal);
260
- return result.status === "retry" ? result.retryAfterMs : undefined;
249
+ const result = await handlers.advance(state.tenantId, state.sessionId, signal);
250
+ return result.status === "busy" ? result.retryAfterMs : undefined;
261
251
  }
262
252
  armTimer(id, timer) {
263
253
  timer.handle = setTimeout(() => {
@@ -305,7 +295,3 @@ export class MemoryExecution {
305
295
  void promise.finally(() => this.inflight.delete(promise));
306
296
  }
307
297
  }
308
- /** Advances and deliveries are queued apart: a session id and an Action id may be equal. */
309
- function stateKey(kind, tenantId, id) {
310
- return `${kind}\u0000${sessionKey(tenantId, id)}`;
311
- }