@nylorun/runtime 0.23.0-beta → 0.24.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 (252) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +79 -28
  3. package/dist/adapters/execution/restate.js +1 -1
  4. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  5. package/dist/adapters/replication/pgoutput.js +6 -3
  6. package/dist/adapters/sandbox/local.d.ts +9 -1
  7. package/dist/adapters/sandbox/local.js +16 -4
  8. package/dist/adapters/sandbox/virtual.js +4 -1
  9. package/dist/api/a2a/endpoint.js +5 -1
  10. package/dist/api/a2a/routes.d.ts +2 -0
  11. package/dist/api/a2a/routes.js +2 -2
  12. package/dist/api/a2a/tasks.js +2 -1
  13. package/dist/api/ag-ui/endpoint.js +4 -2
  14. package/dist/api/ag-ui/history.js +4 -1
  15. package/dist/api/ag-ui/routes.d.ts +4 -1
  16. package/dist/api/ag-ui/routes.js +11 -10
  17. package/dist/api/ag-ui/translate.js +3 -2
  18. package/dist/api/components.d.ts +4953 -1204
  19. package/dist/api/components.js +25 -16
  20. package/dist/api/gate/keys.js +7 -1
  21. package/dist/api/gate/routes.d.ts +6 -0
  22. package/dist/api/gate/routes.js +39 -3
  23. package/dist/api/http/app.js +4 -4
  24. package/dist/api/http/define.d.ts +6 -0
  25. package/dist/api/http/define.js +13 -1
  26. package/dist/api/http/respond.js +1 -1
  27. package/dist/api/http/routes/artifacts.js +35 -8
  28. package/dist/api/http/routes/coverage.d.ts +9 -0
  29. package/dist/api/http/routes/coverage.js +34 -0
  30. package/dist/api/http/routes/environments.d.ts +11 -0
  31. package/dist/api/http/routes/environments.js +283 -0
  32. package/dist/api/http/routes/mcp.d.ts +1 -1
  33. package/dist/api/http/routes/mcp.js +2 -2
  34. package/dist/api/http/routes/me.js +1 -1
  35. package/dist/api/http/routes/reads.d.ts +45 -9
  36. package/dist/api/http/routes/reads.js +54 -5
  37. package/dist/api/http/routes/sessions.js +11 -6
  38. package/dist/api/http/routes/tenant.js +32 -10
  39. package/dist/api/openapi.js +31 -26
  40. package/dist/artifacts/disk.d.ts +59 -0
  41. package/dist/artifacts/disk.js +563 -0
  42. package/dist/artifacts/export.d.ts +1 -1
  43. package/dist/artifacts/export.js +2 -3
  44. package/dist/artifacts/folders.d.ts +6 -3
  45. package/dist/artifacts/folders.js +17 -12
  46. package/dist/artifacts/service.d.ts +2 -1
  47. package/dist/artifacts/service.js +6 -1
  48. package/dist/artifacts/workspace.d.ts +1 -1
  49. package/dist/artifacts/workspace.js +1 -1
  50. package/dist/core/flow-host.d.ts +2 -3
  51. package/dist/core/flow-host.js +3 -0
  52. package/dist/core/provider.d.ts +3 -2
  53. package/dist/core/provider.js +5 -37
  54. package/dist/core/sandbox-routes.d.ts +2 -21
  55. package/dist/core/sandbox-routes.js +10 -43
  56. package/dist/execution/types.d.ts +1 -1
  57. package/dist/gates/contract.d.ts +3 -24
  58. package/dist/gates/contract.js +1 -36
  59. package/dist/gates/egress-credentials.d.ts +9 -0
  60. package/dist/gates/egress-credentials.js +11 -4
  61. package/dist/gates/egress.d.ts +28 -1
  62. package/dist/gates/egress.js +115 -10
  63. package/dist/gates/fixture-model.d.ts +28 -0
  64. package/dist/gates/fixture-model.js +92 -0
  65. package/dist/gates/handler.js +4 -4
  66. package/dist/gates/http-tool.d.ts +6 -2
  67. package/dist/gates/http-tool.js +3 -5
  68. package/dist/gates/in-process.d.ts +5 -0
  69. package/dist/gates/in-process.js +12 -1
  70. package/dist/gates/journal.d.ts +30 -0
  71. package/dist/gates/journal.js +52 -0
  72. package/dist/gates/mcp-handler.d.ts +4 -0
  73. package/dist/gates/mcp-handler.js +5 -1
  74. package/dist/gates/meter.d.ts +10 -2
  75. package/dist/gates/meter.js +20 -5
  76. package/dist/gates/policy.d.ts +49 -0
  77. package/dist/gates/policy.js +134 -0
  78. package/dist/gates/tenant-vaults.d.ts +7 -7
  79. package/dist/gates/tenant-vaults.js +5 -5
  80. package/dist/gates/tool-calls.d.ts +10 -5
  81. package/dist/gates/tool-calls.js +28 -10
  82. package/dist/gates/tool-client.d.ts +8 -0
  83. package/dist/gates/tool-client.js +27 -11
  84. package/dist/gates/tool-contract.d.ts +10 -68
  85. package/dist/gates/tool-contract.js +1 -85
  86. package/dist/gates/tool-gate.d.ts +1 -1
  87. package/dist/harness/calls.d.ts +12 -5
  88. package/dist/harness/calls.js +21 -12
  89. package/dist/harness/executors.js +12 -9
  90. package/dist/harness/main.js +18 -5
  91. package/dist/harness/pod.d.ts +34 -0
  92. package/dist/harness/pod.js +41 -0
  93. package/dist/harness/service.d.ts +0 -1
  94. package/dist/harness/service.js +13 -5
  95. package/dist/harness/setup.d.ts +27 -0
  96. package/dist/harness/setup.js +150 -0
  97. package/dist/harness/ws-client.js +3 -2
  98. package/dist/harness/ws-port.d.ts +1 -5
  99. package/dist/harness/ws-port.js +1 -5
  100. package/dist/harness-api/in-process.js +3 -3
  101. package/dist/harness-api/record.d.ts +15 -1
  102. package/dist/harness-api/record.js +174 -44
  103. package/dist/harness-api/redact.d.ts +3 -0
  104. package/dist/harness-api/redact.js +45 -0
  105. package/dist/harness-api/server.d.ts +8 -0
  106. package/dist/harness-api/server.js +93 -13
  107. package/dist/harness-api/start.d.ts +10 -3
  108. package/dist/harness-api/start.js +40 -10
  109. package/dist/harness-api/tool-journal.d.ts +96 -0
  110. package/dist/harness-api/tool-journal.js +102 -0
  111. package/dist/harness-api/tool-status.d.ts +13 -0
  112. package/dist/harness-api/tool-status.js +26 -0
  113. package/dist/harness-api/workspace.d.ts +6 -3
  114. package/dist/harness-api/workspace.js +27 -11
  115. package/dist/harness-api/ws-server.d.ts +11 -3
  116. package/dist/harness-api/ws-server.js +94 -15
  117. package/dist/host/gates.d.ts +2 -1
  118. package/dist/host/gates.js +10 -2
  119. package/dist/host/main.js +54 -5
  120. package/dist/host/operate.js +67 -5
  121. package/dist/host/stack-config.d.ts +64 -0
  122. package/dist/host/stack-config.js +61 -1
  123. package/dist/keys/client.js +4 -1
  124. package/dist/keys/keys.d.ts +5 -5
  125. package/dist/keys/keys.js +5 -7
  126. package/dist/management-openapi.json +999 -2211
  127. package/dist/mcp/connect.d.ts +25 -20
  128. package/dist/mcp/connect.js +29 -30
  129. package/dist/mcp/pool.d.ts +2 -2
  130. package/dist/mcp/pool.js +1 -1
  131. package/dist/mcp/preview.d.ts +3 -3
  132. package/dist/mcp/preview.js +10 -20
  133. package/dist/mcp/snapshot.d.ts +5 -11
  134. package/dist/mcp/snapshot.js +6 -7
  135. package/dist/model/pi-model.js +8 -0
  136. package/dist/openapi.json +6955 -2526
  137. package/dist/reads/types.d.ts +16 -3
  138. package/dist/record/append.d.ts +3 -1
  139. package/dist/record/append.js +1 -0
  140. package/dist/record/envelope.d.ts +5 -1
  141. package/dist/record/envelope.js +7 -3
  142. package/dist/record/environment.d.ts +25 -0
  143. package/dist/record/environment.js +44 -0
  144. package/dist/record/index.d.ts +1 -1
  145. package/dist/record/index.js +1 -1
  146. package/dist/sandbox/disk-fs.d.ts +95 -0
  147. package/dist/sandbox/disk-fs.js +206 -0
  148. package/dist/sandbox/join.d.ts +17 -18
  149. package/dist/sandbox/join.js +42 -15
  150. package/dist/sandbox/manager.d.ts +16 -8
  151. package/dist/sandbox/manager.js +33 -17
  152. package/dist/sandbox/placement.d.ts +13 -12
  153. package/dist/sandbox/placement.js +29 -12
  154. package/dist/sandbox/pods/client.d.ts +6 -1
  155. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  156. package/dist/sandbox/pods/lifecycle.js +14 -26
  157. package/dist/sandbox/pods/reconcile.js +18 -12
  158. package/dist/sandbox/pods/spec.d.ts +15 -13
  159. package/dist/sandbox/pods/spec.js +16 -20
  160. package/dist/sandbox/records.d.ts +2 -2
  161. package/dist/sandbox/records.js +3 -3
  162. package/dist/sandbox/resolve.d.ts +35 -27
  163. package/dist/sandbox/resolve.js +66 -41
  164. package/dist/sandbox/session-sandbox.js +1 -1
  165. package/dist/sandbox/setup.d.ts +61 -0
  166. package/dist/sandbox/setup.js +123 -0
  167. package/dist/sandbox/share.d.ts +11 -21
  168. package/dist/sandbox/share.js +12 -41
  169. package/dist/sandbox/tenant-config.d.ts +12 -14
  170. package/dist/sandbox/tenant-config.js +12 -17
  171. package/dist/sandbox/types.d.ts +6 -0
  172. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  173. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  174. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  175. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  176. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  177. package/dist/store/postgres/reads.js +117 -17
  178. package/dist/store/postgres/record-writer.d.ts +4 -4
  179. package/dist/store/postgres/record-writer.js +9 -9
  180. package/dist/store/postgres/record.d.ts +7 -0
  181. package/dist/store/postgres/record.js +23 -2
  182. package/dist/store/postgres/schema.d.ts +934 -761
  183. package/dist/store/postgres/schema.js +102 -73
  184. package/dist/store/postgres/store.js +119 -66
  185. package/dist/store/types.d.ts +109 -48
  186. package/dist/tenant/advance.d.ts +7 -6
  187. package/dist/tenant/advance.js +115 -42
  188. package/dist/tenant/artifact-tool.js +3 -4
  189. package/dist/tenant/commands.d.ts +15 -2
  190. package/dist/tenant/commands.js +93 -12
  191. package/dist/tenant/context.d.ts +48 -22
  192. package/dist/tenant/context.js +4 -15
  193. package/dist/tenant/effects.d.ts +1 -1
  194. package/dist/tenant/effects.js +37 -20
  195. package/dist/tenant/environments.d.ts +167 -0
  196. package/dist/tenant/environments.js +823 -0
  197. package/dist/tenant/harness-tokens.d.ts +21 -0
  198. package/dist/tenant/harness-tokens.js +61 -0
  199. package/dist/tenant/host-token.d.ts +1 -1
  200. package/dist/tenant/host-token.js +2 -4
  201. package/dist/tenant/identity-file.d.ts +4 -4
  202. package/dist/tenant/identity-file.js +17 -8
  203. package/dist/tenant/issuers.js +3 -3
  204. package/dist/tenant/principals.js +1 -1
  205. package/dist/tenant/reset.js +3 -3
  206. package/dist/tenant/runtime.d.ts +6 -1
  207. package/dist/tenant/runtime.js +7 -1
  208. package/dist/tenant/session.d.ts +3 -3
  209. package/dist/tenant/session.js +0 -14
  210. package/dist/tenant/sessions.d.ts +13 -13
  211. package/dist/tenant/sessions.js +61 -138
  212. package/dist/tenant/signing-keys.d.ts +5 -1
  213. package/dist/tenant/status.js +3 -3
  214. package/dist/tenant/sweep.js +12 -6
  215. package/dist/tenant/transcript.d.ts +210 -20
  216. package/dist/tenant/transcript.js +62 -11
  217. package/dist/tenant/turn-timing.d.ts +43 -0
  218. package/dist/tenant/turn-timing.js +39 -0
  219. package/dist/tenant/types.d.ts +2 -0
  220. package/dist/traces/ids.d.ts +42 -0
  221. package/dist/traces/ids.js +85 -0
  222. package/dist/traces/key.d.ts +6 -0
  223. package/dist/traces/key.js +37 -0
  224. package/dist/traces/map.d.ts +113 -0
  225. package/dist/traces/map.js +317 -0
  226. package/dist/traces/options.d.ts +7 -0
  227. package/dist/traces/options.js +1 -0
  228. package/dist/traces/otlp.d.ts +42 -0
  229. package/dist/traces/otlp.js +84 -0
  230. package/dist/traces/projector.d.ts +58 -0
  231. package/dist/traces/projector.js +200 -0
  232. package/dist/traces/read.d.ts +23 -0
  233. package/dist/traces/read.js +16 -0
  234. package/dist/vault/coverage.d.ts +26 -0
  235. package/dist/vault/coverage.js +64 -0
  236. package/dist/vault/error.d.ts +11 -1
  237. package/dist/vault/error.js +5 -1
  238. package/dist/vault/service.d.ts +47 -48
  239. package/dist/vault/service.js +321 -382
  240. package/dist/vault/sources.d.ts +5 -16
  241. package/dist/vault/sources.js +6 -8
  242. package/dist/version.d.ts +1 -1
  243. package/dist/version.js +2 -2
  244. package/package.json +13 -13
  245. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  246. package/dist/api/http/routes/sandboxes.js +0 -153
  247. package/dist/api/http/routes/vaults.d.ts +0 -10
  248. package/dist/api/http/routes/vaults.js +0 -139
  249. package/dist/record/sandbox.d.ts +0 -25
  250. package/dist/record/sandbox.js +0 -43
  251. package/dist/tenant/sandboxes.d.ts +0 -123
  252. package/dist/tenant/sandboxes.js +0 -566
@@ -40,10 +40,13 @@
40
40
  * already recorded; an `ownership.lost` leaves the session to the advance that owns it.
41
41
  */
42
42
  import { runDurable, runFlowDurable, } from "@nylorun/harness/run";
43
+ import { redactTokens } from "../harness-api/redact.js";
43
44
  import { aggregateWaits, cancelSiblingWork, isWorkflowManifest, wakeLinkedWorkflow, } from "../core/flow-host.js";
44
45
  import { isOwnershipLost, ownedTx } from "../store/ownership.js";
45
46
  import { endAdvance } from "./scheduler.js";
46
- import { isGateToolEffect, recoversModelCalls, recoversToolCalls } from "./effects.js";
47
+ import { harnessOf } from "./context.js";
48
+ import { HARNESS_ID } from "../sandbox/placement.js";
49
+ import { recoversModelCalls } from "./effects.js";
47
50
  import { slimModelEffects } from "./slim.js";
48
51
  import { applyUpdates, leanState, transcriptShadow, withTranscript, } from "./history.js";
49
52
  import { command } from "./commands.js";
@@ -51,7 +54,8 @@ import { exportOutputs } from "../artifacts/export.js";
51
54
  import { dropRunGrant, grantRun } from "./run-grants.js";
52
55
  import { startHeartbeat } from "../harness-api/renew.js";
53
56
  import { buildTurnStart, startSegment } from "../harness-api/start.js";
54
- import { touchPodSandbox } from "./sandboxes.js";
57
+ import { touchEnvironment } from "./environments.js";
58
+ import { endTurn, pauseTurn } from "./turn-timing.js";
55
59
  import { AdvanceAbort, AdvanceDeadlineError, abortKind, } from "./worker.js";
56
60
  /**
57
61
  * A segment stopped by a `shutdown` or `ownership.lost` abort before it settled. Never
@@ -179,28 +183,27 @@ export async function advance(ctx, id, signal) {
179
183
  sessionId: id,
180
184
  message: error instanceof Error ? error.message : String(error),
181
185
  }));
182
- // A pod sandbox's idle time counts from the end of each segment (D34).
183
- if (placed.pod !== undefined)
184
- await touchPodSandbox(ctx, placed.pod);
186
+ // The Environment was used: `deleteAfter`, and a pod's idle time (D34), count from the end
187
+ // of each segment.
188
+ if (placed.environment !== undefined)
189
+ await touchEnvironment(ctx, placed.environment);
185
190
  }
186
191
  return result;
187
192
  }
188
- /** Why a pod sandbox cannot run a segment, or undefined when it can. */
193
+ /** Why a pod Environment cannot run a segment, or undefined when it can. */
189
194
  function podUnusable(ctx, segment) {
190
195
  const pod = segment.pod;
191
196
  if (!pod)
192
197
  return undefined;
193
198
  if (!ctx.pods)
194
- return `Sandbox ${pod.id} is a pod sandbox, and this Runtime has no sandbox pods`;
199
+ return `Environment ${pod.id} is a pod Environment, and this Runtime has no sandbox pods`;
195
200
  const state = pod.pod;
196
201
  if (state.desired === "deleted")
197
- return `Sandbox ${pod.id} was deleted`;
202
+ return `Environment ${pod.id} was deleted`;
198
203
  if (state.observed === "lost")
199
- return `Sandbox ${pod.id} was lost: ${state.reason ?? "its volume is gone"}`;
200
- if (state.observed === "expired")
201
- return `Sandbox ${pod.id} expired`;
204
+ return `Environment ${pod.id} was lost: ${state.reason ?? "its volume is gone"}`;
202
205
  if (state.observed === "failed")
203
- return `Sandbox ${pod.id} failed to start: ${state.reason ?? "its pod was not ready"}`;
206
+ return `Environment ${pod.id} failed to start: ${state.reason ?? "its pod was not ready"}`;
204
207
  return undefined;
205
208
  }
206
209
  /**
@@ -229,7 +232,6 @@ async function takeOwnership(ctx, id) {
229
232
  if (taken.takeover)
230
233
  await takeOver(t, s, {
231
234
  recoversModelCalls: recoversModelCalls(ctx),
232
- recoversToolCalls: recoversToolCalls(ctx),
233
235
  });
234
236
  if (!s.checkpoint || !["running", "runnable"].includes(s.status)) {
235
237
  await t.releaseOwnership(id, lease.owner, lease.epoch);
@@ -243,10 +245,12 @@ async function takeOwnership(ctx, id) {
243
245
  /**
244
246
  * Takeover (§10.5 step 2, §11.4), inside the transaction that took ownership: the dead
245
247
  * owner's `invoking` effects become `uncertain` and are never invoked again, except model
246
- * calls when the gate recovers them (P1.2) and remote MCP and HTTP tool calls when the Tool
247
- * Gate does (F4.1 G3): those stay `invoking`, and the replay re-sends them. When one belongs
248
- * to the active turn, the session becomes `uncertain` too, with an `effect.uncertain` event.
249
- * Mutates and writes `s`. Returns the effect ids it marked.
248
+ * calls when the gate recovers them (P1.2) and every tool call (R5.3): those stay `invoking`,
249
+ * and the replay decides. It re-sends a model call, and an MCP or HTTP tool call with the same
250
+ * key; a sandbox or core-served tool call fails to the model as `tool.interrupted`
251
+ * (`harness-api/tool-journal.ts`). When an effect marked `uncertain` belongs to the active
252
+ * turn, the session becomes `uncertain` too, with an `effect.uncertain` event. Mutates and
253
+ * writes `s`. Returns the effect ids it marked.
250
254
  */
251
255
  export async function takeOver(t, s, options = {}) {
252
256
  const marked = [];
@@ -254,7 +258,7 @@ export async function takeOver(t, s, options = {}) {
254
258
  // Still running at the gate, or finished there: the replay re-sends it (P1.2).
255
259
  if (options.recoversModelCalls && effect.request.kind === "model")
256
260
  continue;
257
- if (options.recoversToolCalls && isGateToolEffect(s, effect.request))
261
+ if (effect.request.kind === "tool")
258
262
  continue;
259
263
  effect.status = "uncertain";
260
264
  effect.error ??= "The Worker running this effect stopped before its outcome was recorded";
@@ -279,13 +283,16 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
279
283
  let end;
280
284
  try {
281
285
  const segment = await startSegment(ctx, lease, { harness: true });
282
- // A pod sandbox's segment runs in its pod (F7.2), or fails when the pod cannot run it.
286
+ // A pod Environment's segment runs in its pod (F7.2), or fails when the pod cannot run it.
283
287
  const unusable = podUnusable(ctx, segment);
284
288
  if (unusable)
285
289
  throw new Error(unusable);
290
+ // A new volume being seeded from a disk version takes no turn until it holds it (R2d step 3).
291
+ if (segment.pod?.seeding)
292
+ throw new HarnessUnavailable();
286
293
  const pod = segment.pod?.id;
287
- if (pod !== undefined)
288
- placed.pod = pod;
294
+ if (started.environmentId !== undefined)
295
+ placed.environment = started.environmentId;
289
296
  end = await ctx.harness.offer({
290
297
  lease,
291
298
  start: buildTurnStart(ctx, segment),
@@ -293,6 +300,8 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
293
300
  transcript: segment.transcript,
294
301
  run,
295
302
  onTaken,
303
+ // Only a connection of the session's harness takes its runs (R4.1).
304
+ harness: harnessOf(segment.current),
296
305
  ...(pod === undefined ? {} : { pod, waitMs: POD_OFFER_WAIT_MS }),
297
306
  });
298
307
  if (end.kind === "unavailable") {
@@ -310,7 +319,8 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
310
319
  stopIfLeaving(signal);
311
320
  if (end.kind === "released")
312
321
  throw new SegmentStopped(end.reason === "ownership.lost" ? "ownership.lost" : "shutdown");
313
- const { output } = end;
322
+ // A harness's output never carries the Runtime's own tokens into the record (R4).
323
+ const output = redactTokens(end.output);
314
324
  if (output.thrown)
315
325
  throw thrownError(output.thrown);
316
326
  const result = resultOf(output, segment);
@@ -320,7 +330,9 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
320
330
  await settleFailure(ctx, lease, started, signal.reason);
321
331
  else {
322
332
  const recorded = result.status === "yielded" || result.status === "completed" || result.status === "paused";
323
- cursor = await settleTurn(ctx, lease, started, result, recorded ? output.transcript ?? [] : [], signal, segment.cursor);
333
+ cursor = await settleTurn(ctx, lease, started, result, recorded ? output.transcript ?? [] : [], signal, segment.cursor,
334
+ // The turn's end and its transcript are what the harness reported (R4.9).
335
+ { source: { kind: "harness", id: harnessOf(started) }, evidence: "claimed", runId: output.runId });
324
336
  }
325
337
  end.reply(cursor === undefined ? {} : { cursor });
326
338
  }
@@ -361,7 +373,8 @@ function resultOf(output, segment) {
361
373
  effectIds: output.effectIds ?? [],
362
374
  };
363
375
  let state = output.state;
364
- if (state !== undefined && transcriptShadow())
376
+ // Our engine's transcript shadow (tests); another harness's state is stored as sent (R4.2).
377
+ if (state !== undefined && transcriptShadow() && ours(current))
365
378
  state = withTranscript(state, applyUpdates(segment.transcript, output.transcript ?? []));
366
379
  const checkpoint = state === undefined || isWorkflowManifest(current.manifest)
367
380
  ? current.checkpoint
@@ -408,8 +421,8 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
408
421
  * the turn has committed and while the advance still holds the lease. The harness hears back
409
422
  * after the export. The export never throws.
410
423
  */
411
- async function settleTurn(ctx, lease, s, result, updates, signal, cursor) {
412
- const kept = await settle(ctx, lease, s, result, updates, cursor);
424
+ async function settleTurn(ctx, lease, s, result, updates, signal, cursor, writer) {
425
+ const kept = await settle(ctx, lease, s, result, updates, cursor, writer);
413
426
  if (result.status === "completed")
414
427
  await exportOutputs(ctx, lease.sessionId, s.activeTurnId, signal);
415
428
  return kept;
@@ -419,7 +432,9 @@ async function settleTurn(ctx, lease, s, result, updates, signal, cursor) {
419
432
  * cursor of the transcript it recorded (`cursor` when it recorded no edit), or undefined when
420
433
  * the transcript was not kept.
421
434
  */
422
- async function settle(ctx, lease, s, result, updates = [], cursor) {
435
+ async function settle(ctx, lease, s, result, updates = [], cursor,
436
+ /** Who reported the result (R4.9): the run's harness. */
437
+ writer) {
423
438
  const id = lease.sessionId;
424
439
  let kept;
425
440
  const siblingCancelIds = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
@@ -430,11 +445,24 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
430
445
  // A result can arrive during concurrent action persistence. Preserve the runnable marker.
431
446
  const resumeRequested = current.status === "runnable";
432
447
  if (result.status === "waiting" || result.status === "uncertain") {
448
+ // Tool calls the Tool Gate held for approval (R5 step 2): the turn pauses for a person.
449
+ const held = resumeRequested ? [] : await heldCalls(t, result.effectIds);
450
+ if (held.length > 0) {
451
+ current.status = "paused";
452
+ pauseTurn(current);
453
+ current.waits = held;
454
+ const payload = { interactions: held };
455
+ await t.event(id, s.activeTurnId, "turn.paused", payload);
456
+ await t.put("sessions", id, current);
457
+ await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.paused", payload);
458
+ return siblings;
459
+ }
433
460
  const linkedWaits = isWorkflowManifest(current.manifest)
434
461
  ? await aggregateWaits({ t, workflowSessionId: id })
435
462
  : [];
436
463
  if (linkedWaits.length > 0 && !resumeRequested) {
437
464
  current.status = "paused";
465
+ pauseTurn(current);
438
466
  current.waits = linkedWaits;
439
467
  }
440
468
  else {
@@ -456,8 +484,8 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
456
484
  if (result.status === "yielded") {
457
485
  // The turn goes on in a new segment: same turn, next checkpoint, woken right away.
458
486
  const finished = result.checkpoint;
459
- kept = (await recordTranscript(t, id, s.activeTurnId, current, updates)) ?? cursor;
460
- current.state = leanState(result.result.state);
487
+ kept = (await recordTranscript(t, id, s.activeTurnId, current, updates, writer)) ?? cursor;
488
+ current.state = leanOf(current, result.result.state);
461
489
  current.checkpoint = {
462
490
  ...finished,
463
491
  segment: finished.segment + 1,
@@ -478,16 +506,16 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
478
506
  const flow = isWorkflowManifest(current.manifest);
479
507
  if (!flow) {
480
508
  if (result.status !== "failed") {
481
- const last = await recordTranscript(t, id, s.activeTurnId, current, updates);
509
+ const last = await recordTranscript(t, id, s.activeTurnId, current, updates, writer);
482
510
  if (result.status !== "cancelled")
483
511
  kept = last ?? cursor;
484
512
  }
485
513
  current.state =
486
514
  result.status === "failed"
487
515
  ? current.turnStartState
488
- : leanState(result.result.state);
516
+ : leanOf(current, result.result.state);
489
517
  }
490
- current.checkpoint = leanCheckpoint(result.checkpoint);
518
+ current.checkpoint = leanCheckpoint(current, result.checkpoint);
491
519
  current.status = result.status;
492
520
  current.waits =
493
521
  result.status === "paused" ? result.result.pending : undefined;
@@ -508,7 +536,11 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
508
536
  });
509
537
  siblings.push(...cancelResult.agentSessionIds);
510
538
  }
511
- if (result.status !== "paused") {
539
+ // The turn's span (R7.1): a pause starts its wait; an end, its duration and totals.
540
+ const ended = result.status === "paused" ? undefined : await endTurn(t, current, s.activeTurnId);
541
+ if (result.status === "paused")
542
+ pauseTurn(current);
543
+ else {
512
544
  current.activeTurnId = null;
513
545
  if (s.activeTurnId)
514
546
  current.lastTurnId = s.activeTurnId;
@@ -516,7 +548,7 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
516
548
  }
517
549
  const type = `turn.${result.status}`;
518
550
  const payload = result.status === "completed"
519
- ? { output: result.result.output }
551
+ ? { output: result.result.output, ...ended }
520
552
  : result.status === "paused"
521
553
  ? {
522
554
  interactions: (result.result.pending ?? []).map((call) => ({
@@ -539,9 +571,10 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
539
571
  ? { path: result.result.error.path }
540
572
  : {}),
541
573
  },
574
+ ...ended,
542
575
  }
543
- : {};
544
- await t.event(id, s.activeTurnId, type, payload);
576
+ : { ...ended };
577
+ await t.event(id, s.activeTurnId, type, payload, writer);
545
578
  await t.put("sessions", id, current);
546
579
  await wakeWorkflowOf(ctx, t, current, s.activeTurnId, type, payload);
547
580
  return siblings;
@@ -564,6 +597,37 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
564
597
  }
565
598
  return kept;
566
599
  }
600
+ /**
601
+ * The approvals a waiting segment's held tool calls ask for (R5 step 2), as `turn.paused`
602
+ * interactions: each one's id is the call's effect id, which `approve` answers.
603
+ */
604
+ async function heldCalls(t, effectIds) {
605
+ const held = [];
606
+ for (const effectId of effectIds) {
607
+ const row = await t.get("effects", effectId);
608
+ if (row?.status !== "held")
609
+ continue;
610
+ const request = row.request;
611
+ const context = (request.context ?? {});
612
+ const toolName = request.toolName ?? request.key ?? effectId;
613
+ const reason = row.hold?.reason ?? "The Tenant's tool policy asks a person about this call";
614
+ held.push({
615
+ invocationId: typeof context.invocationId === "string" ? context.invocationId : effectId,
616
+ ...(typeof context.callId === "string" ? { callId: context.callId } : {}),
617
+ interaction: {
618
+ id: effectId,
619
+ kind: "approval",
620
+ prompt: `Approve ${toolName}?`,
621
+ reason,
622
+ ...(row.server === undefined ? {} : { server: row.server }),
623
+ input: request.input ?? null,
624
+ },
625
+ status: "interaction",
626
+ toolName,
627
+ });
628
+ }
629
+ return held;
630
+ }
567
631
  /** A segment threw: fail the turn, restore the turn-start state, wake a linked workflow. */
568
632
  async function settleFailure(ctx, lease, s, error) {
569
633
  const id = lease.sessionId;
@@ -574,7 +638,7 @@ async function settleFailure(ctx, lease, s, error) {
574
638
  current.status = "failed";
575
639
  current.state = current.turnStartState;
576
640
  current.error = error instanceof Error ? error.message : String(error);
577
- const payload = { message: current.error };
641
+ const payload = { message: current.error, ...(await endTurn(t, current, current.activeTurnId)) };
578
642
  await t.event(id, current.activeTurnId, "turn.failed", payload);
579
643
  await slimModelEffects(t, id, current.activeTurnId);
580
644
  if (current.activeTurnId)
@@ -584,21 +648,30 @@ async function settleFailure(ctx, lease, s, error) {
584
648
  await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.failed", payload);
585
649
  });
586
650
  }
587
- /** `checkpoint` with its engine state stored lean (`history.ts`). */
588
- function leanCheckpoint(checkpoint) {
651
+ /** Whether the session runs our harness, whose state core may store lean (R4.2). */
652
+ const ours = (session) => harnessOf(session) === HARNESS_ID;
653
+ /**
654
+ * `state` as the session row stores it: our engine's without its transcript (`history.ts`);
655
+ * another harness's as it sent it, never read (R4.2).
656
+ */
657
+ function leanOf(session, state) {
658
+ return ours(session) ? leanState(state) : state;
659
+ }
660
+ /** `checkpoint` with its engine state stored lean (`leanOf`). */
661
+ function leanCheckpoint(session, checkpoint) {
589
662
  const state = checkpoint?.state;
590
663
  if (!checkpoint || !state)
591
664
  return checkpoint;
592
- return { ...checkpoint, state: leanState(state) };
665
+ return { ...checkpoint, state: leanOf(session, state) };
593
666
  }
594
667
  /**
595
668
  * Appends a segment's transcript edits; a snapshot moves `history.snapshot`. Returns the seq of
596
669
  * the last edit, if any.
597
670
  */
598
- async function recordTranscript(t, id, turnId, s, updates) {
671
+ async function recordTranscript(t, id, turnId, s, updates, writer) {
599
672
  let last;
600
673
  for (const update of updates) {
601
- const event = await t.event(id, turnId, "transcript.updated", update);
674
+ const event = await t.event(id, turnId, "transcript.updated", update, writer);
602
675
  if (update.keep === 0)
603
676
  s.history = { from: s.history?.from ?? event.seq, snapshot: event.seq };
604
677
  last = event.seq;
@@ -27,7 +27,7 @@ import { uploadArtifact } from "../artifacts/service.js";
27
27
  import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
28
28
  import { sandboxCapabilityOf } from "../sandbox/manager.js";
29
29
  import { sandboxWorkspaceOf } from "../sandbox/share.js";
30
- import { sandboxLookup, sessionOf } from "./context.js";
30
+ import { sessionOf } from "./context.js";
31
31
  import { HttpError } from "./http.js";
32
32
  import { manifestFor } from "./session.js";
33
33
  import { jsonBytes, utf8Boundary } from "./tool-results.js";
@@ -47,13 +47,12 @@ export async function callSaveArtifact(ctx, request, signal) {
47
47
  const artifactId = typeof input.artifactId === "string" ? input.artifactId : undefined;
48
48
  const { s, workspace, limits, ours } = await ctx.store.tx(async (t) => {
49
49
  const s = await sessionOf(t, request.sessionId);
50
- const lookup = await sandboxLookup(t, s.id);
51
50
  // A new version only of an artifact of this session.
52
51
  const ours = artifactId === undefined ||
53
52
  (isArtifactId(artifactId) && (await t.artifact(artifactId))?.sessionId === s.id);
54
53
  return {
55
54
  s,
56
- workspace: sandboxWorkspaceOf(s, lookup),
55
+ workspace: sandboxWorkspaceOf(s),
57
56
  limits: await readArtifactLimits(t),
58
57
  ours,
59
58
  };
@@ -70,7 +69,7 @@ export async function callSaveArtifact(ctx, request, signal) {
70
69
  id: workspace.ownerId,
71
70
  activeTurnId: s.activeTurnId,
72
71
  manifest: s.manifest,
73
- ...(workspace.sandboxId === undefined ? {} : { sandboxId: workspace.sandboxId }),
72
+ ...(workspace.environmentId === undefined ? {} : { environmentId: workspace.environmentId }),
74
73
  }, capability, path, limits.fileBytes, signal);
75
74
  if (read.kind === "failed")
76
75
  return read;
@@ -1,3 +1,16 @@
1
- import type { SessionCommand } from "@nylorun/core/contracts";
1
+ import type { EventWriter, SessionCommand } from "@nylorun/core/contracts";
2
2
  import { type AuthScope, type TenantContext } from "./context.js";
3
- export declare function command(ctx: TenantContext, id: string, input: SessionCommand, scope: AuthScope): Promise<unknown>;
3
+ import type { TraceContext } from "../traces/ids.js";
4
+ export declare function command(ctx: TenantContext, id: string, input: SessionCommand, scope: AuthScope,
5
+ /**
6
+ * The W3C trace context the request carried (R7.4): a `message` records it on its
7
+ * `command.message`, and the turn joins the caller's trace. Not part of the command's identity.
8
+ */
9
+ options?: {
10
+ trace?: TraceContext;
11
+ }): Promise<unknown>;
12
+ /**
13
+ * Who sent a command (R4.9), from its credential and never its body: a client over the Runtime
14
+ * API (an application, or a person through it or a token), or core itself.
15
+ */
16
+ export declare function commandWriter(scope: AuthScope): EventWriter;
@@ -22,17 +22,24 @@ import { resolveMessageManifest } from "../core/turn-manifest.js";
22
22
  import { canonical } from "../store/canonical.js";
23
23
  import { lockedSession, } from "./context.js";
24
24
  import { fail } from "./http.js";
25
+ import { settleToolRow } from "../harness-api/tool-journal.js";
25
26
  import { accessOf } from "./auth.js";
26
- import { checkSandboxTurn } from "./sandboxes.js";
27
- import { rebaseSessionState, turnManifestOf, variantStore } from "./session.js";
27
+ import { checkEnvironmentTurn } from "./environments.js";
28
+ import { turnManifestOf, variantStore } from "./session.js";
28
29
  import { slimModelEffects } from "./slim.js";
29
30
  import { resolveMessageParts } from "../artifacts/parts.js";
31
+ import { endTurn, resumeTurn, startTurn } from "./turn-timing.js";
30
32
  /** A command's identity for idempotency: everything but the request id. */
31
33
  const semantic = (value) => {
32
34
  const { requestId: _, ...body } = value;
33
35
  return canonical(body);
34
36
  };
35
- export async function command(ctx, id, input, scope) {
37
+ export async function command(ctx, id, input, scope,
38
+ /**
39
+ * The W3C trace context the request carried (R7.4): a `message` records it on its
40
+ * `command.message`, and the turn joins the caller's trace. Not part of the command's identity.
41
+ */
42
+ options = {}) {
36
43
  const { store } = ctx;
37
44
  const cascadeCancelIds = [];
38
45
  const response = await store.tx(async (t) => {
@@ -53,6 +60,8 @@ export async function command(ctx, id, input, scope) {
53
60
  return existing.response;
54
61
  }
55
62
  let event;
63
+ // A tool call the Tool Gate held, which this `approve` answers (R5 step 2).
64
+ let held;
56
65
  // The turn a `cancel` ended, if one was active.
57
66
  let cancelledTurnId = null;
58
67
  if (command.type === "cancel") {
@@ -73,15 +82,24 @@ export async function command(ctx, id, input, scope) {
73
82
  turnId: cancelledTurnId,
74
83
  });
75
84
  if (cancelledTurnId !== null)
76
- for (const effect of await t.effectsForTurn(id, cancelledTurnId, ["invoking"])) {
85
+ for (const effect of await t.effectsForTurn(id, cancelledTurnId, ["invoking", "held"])) {
86
+ // A tool call ends `cancelled` with its `tool.completed` (R5.1); it may have run.
87
+ if (effect.request.kind === "tool") {
88
+ await settleToolRow(t, s, effect, {
89
+ kind: "failed",
90
+ code: "tool.cancelled",
91
+ message: "The turn was cancelled while this call ran; it may have run.",
92
+ }, { cancelled: true });
93
+ continue;
94
+ }
77
95
  effect.status = "uncertain";
78
96
  effect.error =
79
97
  "Turn cancelled before model outcome was durably recorded";
80
98
  await t.put("effects", effect.request.effectId, effect);
81
99
  }
82
- event = await t.event(id, cancelledTurnId, "turn.cancelled", {
83
- reason: command.reason,
84
- });
100
+ // The cancelled turn's span (R7.1): its duration, agent and totals.
101
+ const ended = cancelledTurnId !== null ? await endTurn(t, s, cancelledTurnId) : {};
102
+ event = await t.event(id, cancelledTurnId, "turn.cancelled", { reason: command.reason, ...ended }, commandWriter(scope));
85
103
  await slimModelEffects(t, id, cancelledTurnId);
86
104
  // The process running the advance aborts it on this signal (control.ts), sent at commit.
87
105
  await t.signal({
@@ -110,17 +128,25 @@ export async function command(ctx, id, input, scope) {
110
128
  }
111
129
  }
112
130
  else {
131
+ // A session opened before protocol 11 (migration 0020) keeps its history but takes no turn.
132
+ if (s.closedReason !== undefined)
133
+ fail(409, "This session was opened before protocol 11: open a new session that names an Environment.", { code: "session_closed" });
113
134
  if (command.type === "message") {
114
135
  if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
115
136
  fail(409, "Session has active or unresolved work");
116
137
  // Every turn start on a sandbox: it exists, the token's grants reach it, and no other
117
138
  // session's turn holds it.
118
- await checkSandboxTurn(t, s, scope, ctx);
139
+ await checkEnvironmentTurn(t, s, scope, ctx);
119
140
  s.turnStartState = s.state;
120
141
  // A new turn folds from the latest snapshot: a cancel or failure only reverts this turn.
121
142
  if (s.history?.snapshot !== undefined)
122
143
  s.history = { ...s.history, from: s.history.snapshot };
123
144
  s.activeTurnId = randomUUID();
145
+ startTurn(s);
146
+ if (options.trace)
147
+ s.turnTrace = options.trace;
148
+ else
149
+ delete s.turnTrace;
124
150
  // File parts name artifacts the caller may read; each is pinned to a version now.
125
151
  const parts = "parts" in command
126
152
  ? await resolveMessageParts(t, command.parts, id, accessOf(scope))
@@ -151,7 +177,6 @@ export async function command(ctx, id, input, scope) {
151
177
  fail(400, resolved.message);
152
178
  }
153
179
  else {
154
- rebaseSessionState(s, resolved.hash);
155
180
  s.checkpoint = createDurableCheckpoint({
156
181
  manifest: resolved.manifest,
157
182
  sessionId: id,
@@ -169,6 +194,25 @@ export async function command(ctx, id, input, scope) {
169
194
  }
170
195
  }
171
196
  }
197
+ else if (command.type === "approve" &&
198
+ (held = await heldCall(t, s, command.interactionId)) !== undefined) {
199
+ // A tool call the Tool Gate held for approval (R5 step 2). Approved: the replay sends it
200
+ // again and the gate runs it. Denied: it fails to the model as `tool.denied`, unsent.
201
+ if (s.status !== "paused")
202
+ fail(409, "Session is not awaiting a response");
203
+ if (command.approved) {
204
+ held.status = "invoking";
205
+ const by = principalOf(scope);
206
+ held.approval = { approved: true, at: new Date().toISOString(), ...(by ? { by } : {}) };
207
+ await t.put("effects", held.request.effectId, held);
208
+ }
209
+ else
210
+ await settleToolRow(t, s, held, {
211
+ kind: "failed",
212
+ code: "tool.denied",
213
+ message: "A person denied this call; it was not sent.",
214
+ });
215
+ }
172
216
  else if (isWorkflowManifest(s.manifest)) {
173
217
  // Approvals owned by linked agent sessions must be answered there (WF-R52).
174
218
  const conflict = await foreignInteractionConflict({
@@ -221,10 +265,13 @@ export async function command(ctx, id, input, scope) {
221
265
  segment: (s.checkpoint?.segment ?? 0) + 1,
222
266
  });
223
267
  }
268
+ // A paused turn goes on: its wait for a person is not its running time (R7.1).
269
+ resumeTurn(s);
224
270
  s.status = "runnable";
225
271
  s.waits = undefined;
226
- // A flow answers each interaction in the same segment: the interaction names the cause.
227
- const cause = command.type !== "message" && isWorkflowManifest(s.manifest)
272
+ // A flow answers each interaction in the same segment, as a held tool call's approval is
273
+ // answered (R5 step 2): the interaction names the cause.
274
+ const cause = command.type !== "message" && (isWorkflowManifest(s.manifest) || held !== undefined)
228
275
  ? command.interactionId
229
276
  : s.checkpoint.segment;
230
277
  const commandWake = {
@@ -232,7 +279,7 @@ export async function command(ctx, id, input, scope) {
232
279
  dedupeKey: `${command.type}:${s.activeTurnId}:${cause}`,
233
280
  };
234
281
  await t.wake(id, commandWake);
235
- event = await t.event(id, s.activeTurnId, `command.${command.type}`, command);
282
+ event = await t.event(id, s.activeTurnId, `command.${command.type}`, command.type === "message" && options.trace ? { ...command, trace: options.trace } : command, commandWriter(scope));
236
283
  }
237
284
  await t.put("sessions", id, s);
238
285
  // Direct cancel of a linked agent fails that node with agent.cancelled (WF-R54). The agent
@@ -272,3 +319,37 @@ export async function command(ctx, id, input, scope) {
272
319
  }
273
320
  return response;
274
321
  }
322
+ /** Core's own commands (R4.9): the flow host's, and a drain's cancels. */
323
+ const CORE_PRINCIPALS = { "flow-host": "flow-host", drain: "core" };
324
+ /**
325
+ * Who sent a command (R4.9), from its credential and never its body: a client over the Runtime
326
+ * API (an application, or a person through it or a token), or core itself.
327
+ */
328
+ export function commandWriter(scope) {
329
+ if (scope.kind === "application" && CORE_PRINCIPALS[scope.principalId] !== undefined)
330
+ return {
331
+ source: { kind: "core", id: "runtime" },
332
+ principal: { kind: CORE_PRINCIPALS[scope.principalId], id: scope.principalId },
333
+ };
334
+ const principal = scope.kind === "subject" || scope.kind === "token"
335
+ ? { kind: "subject", id: scope.subject }
336
+ : { kind: "application", id: scope.kind === "anonymous" ? "anonymous" : scope.principalId };
337
+ return { source: { kind: "client", id: principal.id }, principal };
338
+ }
339
+ /** The held tool call of the session's active turn that `interactionId` names (R5 step 2). */
340
+ async function heldCall(t, s, interactionId) {
341
+ const row = await t.get("effects", interactionId);
342
+ if (row?.status !== "held")
343
+ return undefined;
344
+ if (row.request.sessionId !== s.id || row.request.turnId !== s.activeTurnId)
345
+ return undefined;
346
+ return row;
347
+ }
348
+ /** Who answered: the principal (its subject, when it acts for one), or the token's subject. */
349
+ function principalOf(scope) {
350
+ if (scope.kind === "token")
351
+ return `${scope.issuer}:${scope.subject}`;
352
+ if (scope.kind === "subject")
353
+ return `${scope.principalId}:${scope.subject}`;
354
+ return "principalId" in scope ? scope.principalId : undefined;
355
+ }