@nylorun/runtime 0.9.0-beta → 0.10.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 (129) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +96 -25
  3. package/dist/adapters/media.d.ts +2 -2
  4. package/dist/adapters/media.js +2 -2
  5. package/dist/adapters/observe.d.ts +2 -1
  6. package/dist/adapters/observe.js +2 -3
  7. package/dist/assets.d.ts +2 -2
  8. package/dist/assets.js +5 -3
  9. package/dist/core/executors.d.ts +11 -2
  10. package/dist/core/executors.js +20 -4
  11. package/dist/core/flow-host.d.ts +264 -0
  12. package/dist/core/flow-host.js +783 -0
  13. package/dist/core/limits.d.ts +40 -0
  14. package/dist/core/limits.js +76 -0
  15. package/dist/core/runtime.d.ts +5 -90
  16. package/dist/core/runtime.js +3 -1447
  17. package/dist/core/sandbox-routes.d.ts +35 -0
  18. package/dist/core/sandbox-routes.js +106 -0
  19. package/dist/core/store.d.ts +3 -1
  20. package/dist/core/store.js +14 -73
  21. package/dist/core/turn-manifest.d.ts +50 -0
  22. package/dist/core/turn-manifest.js +66 -0
  23. package/dist/host/config-for.d.ts +16 -0
  24. package/dist/host/config-for.js +45 -0
  25. package/dist/host/config.d.ts +24 -0
  26. package/dist/host/create-host.d.ts +21 -0
  27. package/dist/host/create-host.js +396 -0
  28. package/dist/host/environment.d.ts +13 -0
  29. package/dist/host/environment.js +53 -0
  30. package/dist/host/http.d.ts +35 -0
  31. package/dist/host/http.js +136 -0
  32. package/dist/host/index.d.ts +6 -0
  33. package/dist/host/index.js +5 -0
  34. package/dist/host/logger.d.ts +3 -0
  35. package/dist/host/logger.js +18 -0
  36. package/dist/host/main.d.ts +3 -0
  37. package/dist/host/main.js +142 -0
  38. package/dist/index.d.ts +7 -2
  39. package/dist/index.js +4 -2
  40. package/dist/launcher/commands.d.ts +24 -0
  41. package/dist/launcher/commands.js +260 -0
  42. package/dist/launcher/ensure-config.d.ts +16 -0
  43. package/dist/launcher/ensure-config.js +79 -0
  44. package/dist/launcher/errors.d.ts +12 -0
  45. package/dist/launcher/errors.js +22 -0
  46. package/dist/launcher/host-config.d.ts +17 -0
  47. package/dist/launcher/host-config.js +64 -0
  48. package/dist/launcher/host-state.d.ts +17 -0
  49. package/dist/launcher/host-state.js +61 -0
  50. package/dist/launcher/lifecycle.d.ts +66 -0
  51. package/dist/launcher/lifecycle.js +407 -0
  52. package/dist/launcher/locks.d.ts +26 -0
  53. package/dist/launcher/locks.js +117 -0
  54. package/dist/launcher/logs.d.ts +18 -0
  55. package/dist/launcher/logs.js +117 -0
  56. package/dist/launcher/main.d.ts +2 -0
  57. package/dist/launcher/main.js +66 -0
  58. package/dist/launcher/output.d.ts +11 -0
  59. package/dist/launcher/output.js +68 -0
  60. package/dist/launcher/paths.d.ts +20 -0
  61. package/dist/launcher/paths.js +36 -0
  62. package/dist/launcher/probe.d.ts +23 -0
  63. package/dist/launcher/probe.js +72 -0
  64. package/dist/launcher/protocol.d.ts +62 -0
  65. package/dist/launcher/protocol.js +1 -0
  66. package/dist/launcher/schema-guard.d.ts +6 -0
  67. package/dist/launcher/schema-guard.js +36 -0
  68. package/dist/launcher/spawn.d.ts +22 -0
  69. package/dist/launcher/spawn.js +85 -0
  70. package/dist/launcher/status.d.ts +6 -0
  71. package/dist/launcher/status.js +65 -0
  72. package/dist/mcp/connect.d.ts +1 -0
  73. package/dist/mcp/connect.js +2 -0
  74. package/dist/mcp/pool.d.ts +3 -1
  75. package/dist/mcp/pool.js +2 -1
  76. package/dist/mcp/stdio.d.ts +2 -0
  77. package/dist/mcp/stdio.js +3 -1
  78. package/dist/model/auth-store.d.ts +1 -1
  79. package/dist/model/auth-store.js +2 -2
  80. package/dist/model/models.d.ts +4 -0
  81. package/dist/model/models.js +22 -26
  82. package/dist/model/pi-model.js +1 -1
  83. package/dist/model/settings.d.ts +5 -1
  84. package/dist/model/settings.js +6 -2
  85. package/dist/sandbox/select.d.ts +4 -3
  86. package/dist/sandbox/select.js +15 -9
  87. package/dist/sandbox/share.d.ts +23 -0
  88. package/dist/sandbox/share.js +58 -0
  89. package/dist/tenant/envelope.d.ts +10 -0
  90. package/dist/tenant/envelope.js +54 -0
  91. package/dist/tenant/ephemeral.d.ts +31 -0
  92. package/dist/tenant/ephemeral.js +156 -0
  93. package/dist/tenant/logger.d.ts +9 -0
  94. package/dist/tenant/logger.js +25 -0
  95. package/dist/tenant/migration.d.ts +16 -0
  96. package/dist/tenant/migration.js +116 -0
  97. package/dist/tenant/module.d.ts +17 -0
  98. package/dist/tenant/module.js +315 -0
  99. package/dist/tenant/paths.d.ts +24 -0
  100. package/dist/tenant/paths.js +65 -0
  101. package/dist/tenant/pool.d.ts +10 -0
  102. package/dist/tenant/pool.js +54 -0
  103. package/dist/tenant/principals.d.ts +17 -0
  104. package/dist/tenant/principals.js +44 -0
  105. package/dist/tenant/quarantine-error.d.ts +6 -0
  106. package/dist/tenant/quarantine-error.js +15 -0
  107. package/dist/tenant/quarantine.d.ts +35 -0
  108. package/dist/tenant/quarantine.js +94 -0
  109. package/dist/tenant/reset.d.ts +21 -0
  110. package/dist/tenant/reset.js +125 -0
  111. package/dist/tenant/runtime.d.ts +80 -0
  112. package/dist/tenant/runtime.js +2256 -0
  113. package/dist/tenant/schema.d.ts +23 -0
  114. package/dist/tenant/schema.js +151 -0
  115. package/dist/tenant/status.d.ts +28 -0
  116. package/dist/tenant/status.js +95 -0
  117. package/dist/tenant/store-fs.d.ts +16 -0
  118. package/dist/tenant/store-fs.js +205 -0
  119. package/dist/tenant/store-memory.d.ts +19 -0
  120. package/dist/tenant/store-memory.js +100 -0
  121. package/dist/tenant/types.d.ts +125 -0
  122. package/dist/tenant/types.js +1 -0
  123. package/dist/vault/kek.d.ts +5 -2
  124. package/dist/vault/kek.js +7 -10
  125. package/dist/version.d.ts +1 -1
  126. package/dist/version.js +1 -1
  127. package/package.json +9 -6
  128. package/dist/core/main.js +0 -35
  129. /package/dist/{core/main.d.ts → host/config.js} +0 -0
@@ -0,0 +1,783 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mayDispatchMore } from "./limits.js";
3
+ /** Deterministic agent session id: derive(workflowSessionId, path, …parts). */
4
+ export function deriveSessionId(workflowSessionId, path, ...parts) {
5
+ const digest = createHash("sha256")
6
+ .update([workflowSessionId, path, ...parts].join("\0"))
7
+ .digest("hex")
8
+ .slice(0, 24);
9
+ return `wf_${digest}`;
10
+ }
11
+ /**
12
+ * Linked agent session id for a flow agent effect.
13
+ * Verifier agents (`context.role === "verify-agent"`) get a unique session per
14
+ * (path, turnId, iterations) so prior verdicts cannot leak across Loop iterations.
15
+ */
16
+ export function deriveAgentEffectSessionId(workflowSessionId, path, request) {
17
+ if (request.context?.role === "verify-agent") {
18
+ const iterations = request.iterations ?? "-";
19
+ const iterParts = iterations !== "-" && iterations.length > 0 ? iterations.split(".") : [];
20
+ return deriveSessionId(workflowSessionId, path, "verify", request.turnId, ...iterParts);
21
+ }
22
+ return deriveSessionId(workflowSessionId, path);
23
+ }
24
+ export function isWorkflowManifest(manifest) {
25
+ return (!!manifest &&
26
+ typeof manifest === "object" &&
27
+ manifest.kind === "workflow");
28
+ }
29
+ /** Flow tool-node effect: `tool` with path/key and no agent capabilityId. */
30
+ export function isFlowToolEffect(request) {
31
+ return (request.kind === "tool" &&
32
+ typeof request.path === "string" &&
33
+ request.path.length > 0 &&
34
+ typeof request.key === "string" &&
35
+ request.key.length > 0 &&
36
+ request.capabilityId === undefined);
37
+ }
38
+ export function isFlowEffect(request) {
39
+ return (request.kind === "agent" ||
40
+ request.kind === "fn" ||
41
+ request.kind === "verify" ||
42
+ isFlowToolEffect(request));
43
+ }
44
+ /**
45
+ * Resolve flow effects: agent (via session contract), tool nodes, fn, verify.
46
+ * Runtime never writes agent session state — only issues commands.
47
+ */
48
+ export function resolveFlowEffect(input) {
49
+ const { request, store, session } = input;
50
+ if (!isFlowEffect(request))
51
+ throw new Error(`Not a flow effect: ${request.kind}`);
52
+ const existing = store.get("effects", request.effectId);
53
+ if (existing) {
54
+ if (existing.status === "completed")
55
+ return { status: "completed", outcome: existing.outcome };
56
+ if (existing.status === "queued") {
57
+ if (input.limits &&
58
+ !mayDispatchMore(countActiveFlowWork(store, session.id, request.turnId), input.limits))
59
+ return { status: "pending" };
60
+ return dispatchQueuedEffect(input, existing);
61
+ }
62
+ if (request.kind === "agent")
63
+ return settleAgentEffect(input, existing) ?? { status: "pending" };
64
+ return {
65
+ status: existing.status === "uncertain" || existing.status === "invoking"
66
+ ? "uncertain"
67
+ : "pending",
68
+ };
69
+ }
70
+ if (input.limits &&
71
+ !mayDispatchMore(countActiveFlowWork(store, session.id, request.turnId), input.limits)) {
72
+ store.put("effects", request.effectId, {
73
+ request,
74
+ status: "queued",
75
+ });
76
+ return { status: "pending" };
77
+ }
78
+ if (request.kind === "agent")
79
+ return startAgentEffect(input);
80
+ if (isFlowToolEffect(request))
81
+ return startToolNode(input);
82
+ return startFnOrVerify(input);
83
+ }
84
+ /** Active agent turns + pending/claimed actions for one workflow turn. */
85
+ export function countActiveFlowWork(store, workflowSessionId, turnId) {
86
+ let n = 0;
87
+ for (const effect of store.all("effects")) {
88
+ if (effect.request?.sessionId !== workflowSessionId)
89
+ continue;
90
+ if (effect.request?.turnId !== turnId)
91
+ continue;
92
+ if (effect.status === "queued")
93
+ continue;
94
+ if (effect.status === "completed" || effect.status === "cancelled")
95
+ continue;
96
+ if (effect.request?.kind === "agent" && effect.status === "pending") {
97
+ n += 1;
98
+ continue;
99
+ }
100
+ if (effect.request?.kind === "fn" ||
101
+ effect.request?.kind === "verify" ||
102
+ isFlowToolEffect(effect.request)) {
103
+ const action = store.get("actions", effect.request.effectId);
104
+ if (action &&
105
+ (action.status === "pending" || action.status === "claimed"))
106
+ n += 1;
107
+ }
108
+ }
109
+ return n;
110
+ }
111
+ function dispatchQueuedEffect(input, existing) {
112
+ const { request, store } = input;
113
+ store.put("effects", request.effectId, {
114
+ ...existing,
115
+ status: "pending",
116
+ });
117
+ if (request.kind === "agent")
118
+ return startAgentEffect(input);
119
+ if (isFlowToolEffect(request))
120
+ return startToolNode(input);
121
+ return startFnOrVerify(input);
122
+ }
123
+ function startFnOrVerify(input) {
124
+ const { request, store, session } = input;
125
+ store.put("effects", request.effectId, {
126
+ request,
127
+ status: "pending",
128
+ });
129
+ const kind = request.kind;
130
+ const action = {
131
+ actionId: request.effectId,
132
+ sessionId: request.sessionId,
133
+ turnId: request.turnId,
134
+ agentId: request.agentId,
135
+ manifestHash: request.manifestHash,
136
+ implementationVersion: session.implementationVersion,
137
+ input: request.input,
138
+ context: request.context,
139
+ status: "pending",
140
+ generation: 0,
141
+ claimId: null,
142
+ leaseExpiresAt: null,
143
+ kind,
144
+ path: request.path,
145
+ key: request.key,
146
+ };
147
+ store.put("actions", action.actionId, action);
148
+ const event = store.event(session.id, session.activeTurnId, "action.pending", {
149
+ actionId: action.actionId,
150
+ kind: action.kind,
151
+ path: action.path,
152
+ key: action.key,
153
+ input: action.input,
154
+ });
155
+ input.publish(event);
156
+ input.notify();
157
+ return { status: "pending" };
158
+ }
159
+ function startToolNode(input) {
160
+ const { request, store, session } = input;
161
+ store.put("effects", request.effectId, {
162
+ request,
163
+ status: "pending",
164
+ });
165
+ const action = {
166
+ actionId: request.effectId,
167
+ sessionId: request.sessionId,
168
+ turnId: request.turnId,
169
+ agentId: request.agentId,
170
+ manifestHash: request.manifestHash,
171
+ implementationVersion: session.implementationVersion,
172
+ input: request.input,
173
+ context: request.context,
174
+ status: "pending",
175
+ generation: 0,
176
+ claimId: null,
177
+ leaseExpiresAt: null,
178
+ kind: "tool",
179
+ path: request.path,
180
+ key: request.key,
181
+ };
182
+ store.put("actions", action.actionId, action);
183
+ emitNodeStarted(input, {
184
+ path: request.path,
185
+ kind: "tool",
186
+ key: request.key,
187
+ iterations: request.iterations,
188
+ });
189
+ const event = store.event(session.id, session.activeTurnId, "action.pending", {
190
+ actionId: action.actionId,
191
+ kind: action.kind,
192
+ path: action.path,
193
+ key: action.key,
194
+ input: action.input,
195
+ });
196
+ input.publish(event);
197
+ input.notify();
198
+ return { status: "pending" };
199
+ }
200
+ function startAgentEffect(input) {
201
+ const { request, store, session } = input;
202
+ const body = request.input;
203
+ const path = body.path ?? request.path;
204
+ const agentSessionId = deriveAgentEffectSessionId(session.id, path, request);
205
+ const iterations = request.iterations ?? "-";
206
+ const n = Number(request.context.n ?? iterations.split(".")[0] ?? 1);
207
+ store.put("effects", request.effectId, {
208
+ request,
209
+ status: "pending",
210
+ agentSessionId,
211
+ });
212
+ store.put("links", agentSessionId, {
213
+ workflowSessionId: session.id,
214
+ path,
215
+ effectId: request.effectId,
216
+ turnId: request.turnId,
217
+ });
218
+ const existingAgent = input.readSession(agentSessionId);
219
+ if (!existingAgent) {
220
+ input.putLinkedSession({
221
+ id: agentSessionId,
222
+ agentId: body.agentId,
223
+ ownerUserId: session.ownerUserId,
224
+ vaultIds: session.vaultIds,
225
+ credentialSelections: session.credentialSelections,
226
+ });
227
+ }
228
+ const idempotencyKey = `${request.turnId}:${path}:${iterations}`;
229
+ const messageInput = body.input;
230
+ const accepted = input.sendMessage({
231
+ sessionId: agentSessionId,
232
+ ...(typeof messageInput === "string"
233
+ ? { content: messageInput }
234
+ : { data: messageInput }),
235
+ idempotencyKey,
236
+ requestId: `flow-${request.effectId}`,
237
+ });
238
+ const iterationEvent = store.event(session.id, session.activeTurnId, "loop.iteration", {
239
+ path: String(request.context.loopPath ?? path.split("/")[0]),
240
+ n,
241
+ sessionId: agentSessionId,
242
+ turnId: accepted.turnId ?? undefined,
243
+ });
244
+ input.publish(iterationEvent);
245
+ const nodeAgent = store.event(session.id, session.activeTurnId, "node.agent", {
246
+ path,
247
+ iterations,
248
+ sessionId: agentSessionId,
249
+ turnId: accepted.turnId,
250
+ });
251
+ input.publish(nodeAgent);
252
+ return (settleAgentEffect(input, store.get("effects", request.effectId)) ?? {
253
+ status: "pending",
254
+ });
255
+ }
256
+ function settleAgentEffect(input, effect) {
257
+ if (effect.status === "completed" && effect.outcome)
258
+ return { status: "completed", outcome: effect.outcome };
259
+ const agentSessionId = effect.agentSessionId ??
260
+ deriveAgentEffectSessionId(input.session.id, input.request.input.path ?? input.request.path, input.request);
261
+ const agent = input.readSession(agentSessionId);
262
+ if (!agent)
263
+ return undefined;
264
+ if (agent.status === "completed" && agent.activeTurnId === null) {
265
+ const output = agent.lastOutput ?? null;
266
+ const outcome = { value: output };
267
+ input.store.put("effects", input.request.effectId, {
268
+ request: input.request,
269
+ status: "completed",
270
+ outcome,
271
+ agentSessionId,
272
+ });
273
+ return { status: "completed", outcome };
274
+ }
275
+ if (agent.status === "failed") {
276
+ const outcome = {
277
+ value: {
278
+ kind: "failed",
279
+ code: "agent.failed",
280
+ message: agent.error ?? "Agent turn failed",
281
+ },
282
+ };
283
+ input.store.put("effects", input.request.effectId, {
284
+ request: input.request,
285
+ status: "completed",
286
+ outcome,
287
+ agentSessionId,
288
+ });
289
+ return { status: "completed", outcome };
290
+ }
291
+ if (agent.status === "cancelled") {
292
+ const outcome = {
293
+ value: {
294
+ kind: "failed",
295
+ code: "agent.cancelled",
296
+ message: agent.error ?? "Agent turn was cancelled",
297
+ },
298
+ };
299
+ input.store.put("effects", input.request.effectId, {
300
+ request: input.request,
301
+ status: "completed",
302
+ outcome,
303
+ agentSessionId,
304
+ });
305
+ return { status: "completed", outcome };
306
+ }
307
+ return undefined;
308
+ }
309
+ /** After an agent turn settles, wake the owning workflow if linked. */
310
+ export function wakeLinkedWorkflow(input) {
311
+ const link = input.store.get("links", input.agentSessionId);
312
+ if (!link)
313
+ return;
314
+ const effect = input.store.get("effects", link.effectId);
315
+ if (!effect || effect.status === "completed")
316
+ return;
317
+ const workflow = input.store.get("sessions", link.workflowSessionId);
318
+ if (!workflow)
319
+ return;
320
+ if (workflow.activeTurnId !== link.turnId)
321
+ return;
322
+ const outcome = input.cancelled
323
+ ? {
324
+ value: {
325
+ kind: "failed",
326
+ code: "agent.cancelled",
327
+ message: input.error ?? "Agent turn was cancelled",
328
+ },
329
+ }
330
+ : input.failed
331
+ ? {
332
+ value: {
333
+ kind: "failed",
334
+ code: "agent.failed",
335
+ message: input.error ?? "Agent turn failed",
336
+ },
337
+ }
338
+ : { value: input.output ?? null };
339
+ input.store.put("effects", link.effectId, {
340
+ ...effect,
341
+ status: "completed",
342
+ outcome,
343
+ });
344
+ workflow.status = "runnable";
345
+ input.store.put("sessions", workflow.id, workflow);
346
+ input.schedule(workflow.id);
347
+ }
348
+ /** On Runtime start: settle pending agent effects whose linked turns already finished. */
349
+ export function reconcilePendingAgentEffects(input) {
350
+ for (const effect of input.store.all("effects")) {
351
+ if (effect.request?.kind !== "agent" || effect.status !== "pending")
352
+ continue;
353
+ const id = typeof effect.agentSessionId === "string"
354
+ ? effect.agentSessionId
355
+ : undefined;
356
+ if (!id)
357
+ continue;
358
+ const agent = input.store.get("sessions", id);
359
+ const workflow = input.store.get("sessions", effect.request.sessionId);
360
+ if (workflow && workflow.status === "waiting") {
361
+ workflow.status = "runnable";
362
+ input.store.put("sessions", workflow.id, workflow);
363
+ input.schedule(workflow.id);
364
+ }
365
+ if (!agent)
366
+ continue;
367
+ if (agent.status === "completed") {
368
+ wakeLinkedWorkflow({
369
+ agentSessionId: id,
370
+ store: input.store,
371
+ output: agent.lastOutput ?? null,
372
+ schedule: input.schedule,
373
+ publish: input.publish,
374
+ });
375
+ }
376
+ else if (agent.status === "failed") {
377
+ wakeLinkedWorkflow({
378
+ agentSessionId: id,
379
+ store: input.store,
380
+ failed: true,
381
+ error: agent.error,
382
+ schedule: input.schedule,
383
+ publish: input.publish,
384
+ });
385
+ }
386
+ else if (agent.status === "cancelled") {
387
+ wakeLinkedWorkflow({
388
+ agentSessionId: id,
389
+ store: input.store,
390
+ cancelled: true,
391
+ error: agent.error,
392
+ schedule: input.schedule,
393
+ publish: input.publish,
394
+ });
395
+ }
396
+ else if (agent.status === "running" || agent.status === "runnable") {
397
+ input.schedule(id);
398
+ }
399
+ else if (agent.status === "paused") {
400
+ // Linked pause surfaces on the workflow waits list; keep waiting.
401
+ if (workflow && workflow.status !== "paused") {
402
+ const waits = aggregateWaits({
403
+ store: input.store,
404
+ workflowSessionId: workflow.id,
405
+ });
406
+ if (waits.length > 0) {
407
+ workflow.status = "paused";
408
+ workflow.waits = waits;
409
+ input.store.put("sessions", workflow.id, workflow);
410
+ }
411
+ }
412
+ }
413
+ }
414
+ }
415
+ /** Re-offer orphaned fn/verify claims after a process restart (SD-P7 / WF-C9). */
416
+ export function reofferOrphanedFnVerifyClaims(store) {
417
+ let n = 0;
418
+ for (const action of store.all("actions")) {
419
+ if (action.status === "claimed" &&
420
+ (action.kind === "fn" || action.kind === "verify")) {
421
+ action.status = "pending";
422
+ action.claimId = null;
423
+ action.leaseExpiresAt = null;
424
+ store.put("actions", action.actionId, action);
425
+ const s = store.get("sessions", action.sessionId);
426
+ if (s && (s.status === "waiting" || s.status === "running")) {
427
+ s.status = "runnable";
428
+ store.put("sessions", s.id, s);
429
+ }
430
+ n += 1;
431
+ }
432
+ }
433
+ return n;
434
+ }
435
+ /**
436
+ * On lease expiry: fn/verify → pending again; tool nodes → uncertain (WF-R35, WF-R38).
437
+ * Returns whether any fn/verify was redelivered.
438
+ */
439
+ export function applyFlowLeaseExpiry(input) {
440
+ if (input.action.status !== "claimed" || !input.action.leaseExpiresAt)
441
+ return "skip";
442
+ const now = input.now ?? Date.now();
443
+ if (Date.parse(input.action.leaseExpiresAt) > now)
444
+ return "skip";
445
+ if (input.action.kind === "fn" || input.action.kind === "verify") {
446
+ input.action.status = "pending";
447
+ input.action.claimId = null;
448
+ input.action.leaseExpiresAt = null;
449
+ input.store.put("actions", input.action.actionId, input.action);
450
+ return "reoffer";
451
+ }
452
+ if (input.action.kind === "tool" &&
453
+ "path" in input.action &&
454
+ !("capabilityId" in input.action)) {
455
+ input.action.status = "uncertain";
456
+ input.store.put("actions", input.action.actionId, input.action);
457
+ const effect = input.store.get("effects", input.action.actionId);
458
+ if (effect) {
459
+ effect.status = "uncertain";
460
+ input.store.put("effects", input.action.actionId, effect);
461
+ }
462
+ return "uncertain";
463
+ }
464
+ return "skip";
465
+ }
466
+ /** Emit loop.verified / loop.decided after fn/verify actions complete. */
467
+ export function emitLoopActionEvents(input) {
468
+ const { action, store, session } = input;
469
+ if (action.kind !== "fn" && action.kind !== "verify")
470
+ return;
471
+ const n = Number(action.context?.n ?? 1);
472
+ const path = String(action.context?.loopPath ?? action.path ?? "");
473
+ if (action.kind === "verify") {
474
+ const verdict = input.value;
475
+ if (verdict?.kind === "failed")
476
+ return;
477
+ const event = store.event(session.id, session.activeTurnId, "loop.verified", {
478
+ path,
479
+ n,
480
+ pass: Boolean(verdict?.pass),
481
+ ...(verdict?.feedback !== undefined
482
+ ? { feedback: verdict.feedback }
483
+ : {}),
484
+ ...(verdict?.data !== undefined ? { data: verdict.data } : {}),
485
+ });
486
+ input.publish(event);
487
+ return;
488
+ }
489
+ if (action.context?.role !== "decide")
490
+ return;
491
+ const decision = input.value;
492
+ const event = store.event(session.id, session.activeTurnId, "loop.decided", {
493
+ path,
494
+ n,
495
+ next: "output" in decision && !("input" in decision) ? "output" : "input",
496
+ patched: Boolean(decision.agent),
497
+ });
498
+ input.publish(event);
499
+ }
500
+ export function emitNodeStarted(input, payload) {
501
+ const event = input.store.event(input.session.id, input.session.activeTurnId, "node.started", payload);
502
+ input.publish(event);
503
+ return event;
504
+ }
505
+ export function emitNodeCompleted(input, payload) {
506
+ const event = input.store.event(input.session.id, input.session.activeTurnId, "node.completed", payload);
507
+ input.publish(event);
508
+ return event;
509
+ }
510
+ export function emitNodeFailed(input, payload) {
511
+ const event = input.store.event(input.session.id, input.session.activeTurnId, "node.failed", payload);
512
+ input.publish(event);
513
+ return event;
514
+ }
515
+ export function emitSwitchSelected(input, payload) {
516
+ const event = input.store.event(input.session.id, input.session.activeTurnId, "switch.selected", payload);
517
+ input.publish(event);
518
+ return event;
519
+ }
520
+ export function emitMapItems(input, payload) {
521
+ const event = input.store.event(input.session.id, input.session.activeTurnId, "map.items", payload);
522
+ input.publish(event);
523
+ return event;
524
+ }
525
+ /** Path depth for deepest-first cancel ordering. */
526
+ export function pathDepth(path) {
527
+ if (!path)
528
+ return 0;
529
+ return path.split("/").filter(Boolean).length;
530
+ }
531
+ /**
532
+ * Plan a cancel cascade for a workflow session: linked agents deepest first,
533
+ * then pending → cancelled and claimed → uncertain actions (SD-P11 / WF-R53).
534
+ */
535
+ export function planCancelCascade(input) {
536
+ // links table is keyed by agentSessionId — discover via sessions + get(links, id).
537
+ const agentEntries = [];
538
+ for (const sess of input.store.all("sessions")) {
539
+ const link = input.store.get("links", sess.id);
540
+ if (!link || link.workflowSessionId !== input.workflowSessionId)
541
+ continue;
542
+ agentEntries.push({ sessionId: sess.id, path: link.path });
543
+ }
544
+ agentEntries.sort((a, b) => pathDepth(b.path) - pathDepth(a.path));
545
+ const pendingActionIds = [];
546
+ const claimedActionIds = [];
547
+ for (const action of input.store.all("actions")) {
548
+ if (action.sessionId !== input.workflowSessionId)
549
+ continue;
550
+ if (input.turnId !== null && action.turnId !== input.turnId)
551
+ continue;
552
+ if (action.status === "pending")
553
+ pendingActionIds.push(action.actionId);
554
+ else if (action.status === "claimed")
555
+ claimedActionIds.push(action.actionId);
556
+ }
557
+ return {
558
+ agentSessionIds: agentEntries.map((e) => e.sessionId),
559
+ pendingActionIds,
560
+ claimedActionIds,
561
+ };
562
+ }
563
+ /**
564
+ * Cancel sibling work under a Parallel/Map parent when one branch fails (PAR-R6).
565
+ * Agent turns listed in `agentSessionIds` are returned for the caller to cancel;
566
+ * pending actions → cancelled, claimed tool actions → uncertain.
567
+ * When `cancelEffectIds` is provided, those effects are marked cancelled and their
568
+ * paths are included in the sibling path set.
569
+ */
570
+ export function cancelSiblingWork(input) {
571
+ const cancelledActions = [];
572
+ const uncertainActions = [];
573
+ const agentSessionIds = [];
574
+ const siblingPaths = new Set(input.siblingPaths ?? []);
575
+ for (const effectId of input.cancelEffectIds ?? []) {
576
+ const effect = input.store.get("effects", effectId);
577
+ if (!effect)
578
+ continue;
579
+ const path = typeof effect.request?.path === "string"
580
+ ? effect.request.path
581
+ : undefined;
582
+ if (path)
583
+ siblingPaths.add(path);
584
+ if (effect.status === "pending" ||
585
+ effect.status === "queued" ||
586
+ effect.status === "uncertain") {
587
+ effect.status = "cancelled";
588
+ input.store.put("effects", effectId, effect);
589
+ }
590
+ }
591
+ const matchesSibling = (path) => {
592
+ if (!path)
593
+ return false;
594
+ return [...siblingPaths].some((sib) => path === sib || path.startsWith(`${sib}/`));
595
+ };
596
+ for (const sess of input.store.all("sessions")) {
597
+ const link = input.store.get("links", sess.id);
598
+ if (!link || link.workflowSessionId !== input.workflowSessionId)
599
+ continue;
600
+ if (!matchesSibling(link.path))
601
+ continue;
602
+ if (sess.activeTurnId &&
603
+ ["running", "runnable", "paused", "waiting"].includes(sess.status))
604
+ agentSessionIds.push(sess.id);
605
+ }
606
+ for (const action of input.store.all("actions")) {
607
+ if (action.sessionId !== input.workflowSessionId)
608
+ continue;
609
+ if (action.turnId !== input.turnId)
610
+ continue;
611
+ if (!matchesSibling(action.path))
612
+ continue;
613
+ if (action.status === "pending") {
614
+ action.status = "cancelled";
615
+ input.store.put("actions", action.actionId, action);
616
+ const effect = input.store.get("effects", action.actionId);
617
+ if (effect) {
618
+ effect.status = "cancelled";
619
+ input.store.put("effects", action.actionId, effect);
620
+ }
621
+ cancelledActions.push(action.actionId);
622
+ }
623
+ else if (action.status === "claimed") {
624
+ action.status = "uncertain";
625
+ input.store.put("actions", action.actionId, action);
626
+ const effect = input.store.get("effects", action.actionId);
627
+ if (effect) {
628
+ effect.status = "uncertain";
629
+ input.store.put("effects", action.actionId, effect);
630
+ }
631
+ uncertainActions.push(action.actionId);
632
+ }
633
+ }
634
+ return { cancelledActions, uncertainActions, agentSessionIds };
635
+ }
636
+ /**
637
+ * Apply cancel fencing to workflow-session actions (pending→cancelled, claimed→uncertain).
638
+ */
639
+ export function fenceWorkflowActions(input) {
640
+ const cancelled = [];
641
+ const uncertain = [];
642
+ for (const action of input.store.all("actions")) {
643
+ if (action.sessionId !== input.workflowSessionId)
644
+ continue;
645
+ if (input.turnId !== null && action.turnId !== input.turnId)
646
+ continue;
647
+ if (!["pending", "claimed"].includes(action.status))
648
+ continue;
649
+ const next = action.status === "claimed" ? "uncertain" : "cancelled";
650
+ action.status = next;
651
+ input.store.put("actions", action.actionId, action);
652
+ const effect = input.store.get("effects", action.actionId);
653
+ if (effect) {
654
+ effect.status = next;
655
+ input.store.put("effects", action.actionId, effect);
656
+ }
657
+ if (next === "cancelled")
658
+ cancelled.push(action.actionId);
659
+ else
660
+ uncertain.push(action.actionId);
661
+ }
662
+ // Drop queued effects that never started.
663
+ for (const effect of input.store.all("effects")) {
664
+ if (effect.request?.sessionId !== input.workflowSessionId)
665
+ continue;
666
+ if (input.turnId !== null && effect.request?.turnId !== input.turnId)
667
+ continue;
668
+ if (effect.status === "queued") {
669
+ effect.status = "cancelled";
670
+ input.store.put("effects", effect.request.effectId, effect);
671
+ cancelled.push(effect.request.effectId);
672
+ }
673
+ }
674
+ return { cancelled, uncertain };
675
+ }
676
+ /**
677
+ * Aggregate human waits across the workflow session and linked agent sessions (WF-R51).
678
+ */
679
+ export function aggregateWaits(input) {
680
+ const waits = [];
681
+ const workflow = input.store.get("sessions", input.workflowSessionId);
682
+ if (!workflow)
683
+ return waits;
684
+ const pushFromSession = (session, path, raw) => {
685
+ if (!Array.isArray(raw))
686
+ return;
687
+ for (const call of raw) {
688
+ const interaction = call.interaction ?? call;
689
+ const interactionId = String(interaction?.id ?? call.interactionId ?? "");
690
+ if (!interactionId)
691
+ continue;
692
+ waits.push({
693
+ sessionId: session.id,
694
+ path,
695
+ interactionId,
696
+ kind: String(interaction?.kind ?? call.kind ?? "approval"),
697
+ ...(call.invocationId !== undefined
698
+ ? { invocationId: String(call.invocationId) }
699
+ : {}),
700
+ ...(call.wait !== undefined ? { wait: call.wait } : {}),
701
+ ...(call.status !== undefined ? { status: String(call.status) } : {}),
702
+ });
703
+ }
704
+ };
705
+ // Workflow-owned interactions (tool-node / verify approvals) live on the workflow session.
706
+ if (Array.isArray(workflow.waits))
707
+ pushFromSession(workflow, "", workflow.waits);
708
+ else if (workflow.waits &&
709
+ typeof workflow.waits === "object" &&
710
+ Array.isArray(workflow.waits.interactions))
711
+ pushFromSession(workflow, "", workflow.waits.interactions);
712
+ for (const sess of input.store.all("sessions")) {
713
+ const link = input.store.get("links", sess.id);
714
+ if (!link || link.workflowSessionId !== input.workflowSessionId)
715
+ continue;
716
+ if (sess.status !== "paused")
717
+ continue;
718
+ pushFromSession(sess, link.path, sess.waits);
719
+ }
720
+ return waits;
721
+ }
722
+ /**
723
+ * Look up which session owns an interaction id. Used for 409 foreign approvals (WF-R52).
724
+ */
725
+ export function findInteractionOwner(input) {
726
+ const waits = aggregateWaits(input);
727
+ const hit = waits.find((w) => w.interactionId === input.interactionId);
728
+ if (hit)
729
+ return { sessionId: hit.sessionId, path: hit.path };
730
+ // Also scan linked paused sessions' plan-shaped waits that aggregateWaits may have missed.
731
+ for (const sess of input.store.all("sessions")) {
732
+ const link = input.store.get("links", sess.id);
733
+ if (!link || link.workflowSessionId !== input.workflowSessionId)
734
+ continue;
735
+ const raw = sess.waits;
736
+ if (!Array.isArray(raw))
737
+ continue;
738
+ for (const call of raw) {
739
+ const id = String(call.interaction?.id ?? call.interactionId ?? "");
740
+ if (id === input.interactionId)
741
+ return { sessionId: sess.id, path: link.path };
742
+ }
743
+ }
744
+ return undefined;
745
+ }
746
+ /**
747
+ * If approve/respond targets an interaction owned by a linked agent session,
748
+ * return a 409 message naming that session (WF-R52 / LOOP-A4).
749
+ */
750
+ export function foreignInteractionConflict(input) {
751
+ const owner = findInteractionOwner(input);
752
+ if (!owner)
753
+ return undefined;
754
+ if (owner.sessionId === input.workflowSessionId)
755
+ return undefined;
756
+ return {
757
+ status: 409,
758
+ message: `Interaction belongs to session ${owner.sessionId}`,
759
+ ownerSessionId: owner.sessionId,
760
+ };
761
+ }
762
+ /**
763
+ * When concurrency slots free, re-enter the workflow so queued effects can start
764
+ * (status stays `queued` until `resolveFlowEffect` dispatches them).
765
+ */
766
+ export function wakeForQueuedEffects(input) {
767
+ if (!mayDispatchMore(countActiveFlowWork(input.store, input.workflowSessionId, input.turnId), input.limits))
768
+ return false;
769
+ const hasQueued = input.store
770
+ .all("effects")
771
+ .some((e) => e.status === "queued" &&
772
+ e.request?.sessionId === input.workflowSessionId &&
773
+ e.request?.turnId === input.turnId);
774
+ if (!hasQueued)
775
+ return false;
776
+ const workflow = input.store.get("sessions", input.workflowSessionId);
777
+ if (workflow && workflow.status === "waiting") {
778
+ workflow.status = "runnable";
779
+ input.store.put("sessions", workflow.id, workflow);
780
+ }
781
+ input.schedule(input.workflowSessionId);
782
+ return true;
783
+ }