@nylorun/runtime 0.15.2-beta → 0.17.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 (291) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +68 -29
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/sandbox/local.d.ts +10 -0
  6. package/dist/adapters/sandbox/local.js +195 -0
  7. package/dist/adapters/sandbox/virtual.js +36 -1
  8. package/dist/admin-openapi.json +341 -7
  9. package/dist/api/a2a/tasks.js +6 -1
  10. package/dist/api/ag-ui/endpoint.js +1 -2
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.d.ts +1 -1
  13. package/dist/api/ag-ui/routes.js +15 -13
  14. package/dist/api/components.d.ts +2667 -711
  15. package/dist/api/components.js +24 -11
  16. package/dist/api/gate/keys.d.ts +4 -3
  17. package/dist/api/gate/routes.d.ts +19 -3
  18. package/dist/api/gate/routes.js +162 -53
  19. package/dist/api/http/app.js +6 -0
  20. package/dist/api/http/define.d.ts +19 -12
  21. package/dist/api/http/define.js +13 -27
  22. package/dist/api/http/routes/access.d.ts +3 -19
  23. package/dist/api/http/routes/access.js +12 -172
  24. package/dist/api/http/routes/actions.js +0 -1
  25. package/dist/api/http/routes/artifacts.d.ts +11 -0
  26. package/dist/api/http/routes/artifacts.js +516 -0
  27. package/dist/api/http/routes/me.d.ts +13 -0
  28. package/dist/api/http/routes/me.js +45 -0
  29. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  30. package/dist/api/http/routes/sandboxes.js +150 -0
  31. package/dist/api/http/routes/sessions.js +9 -13
  32. package/dist/api/http/routes/tenant.js +36 -2
  33. package/dist/api/http/routes/vaults.d.ts +12 -3
  34. package/dist/api/http/routes/vaults.js +122 -54
  35. package/dist/api/openapi.js +4 -10
  36. package/dist/artifacts/config.d.ts +23 -0
  37. package/dist/artifacts/config.js +27 -0
  38. package/dist/artifacts/export.d.ts +8 -0
  39. package/dist/artifacts/export.js +205 -0
  40. package/dist/artifacts/files.d.ts +36 -0
  41. package/dist/artifacts/files.js +45 -0
  42. package/dist/artifacts/folders.d.ts +60 -0
  43. package/dist/artifacts/folders.js +209 -0
  44. package/dist/artifacts/links.d.ts +28 -0
  45. package/dist/artifacts/links.js +92 -0
  46. package/dist/artifacts/media-types.d.ts +13 -0
  47. package/dist/artifacts/media-types.js +80 -0
  48. package/dist/artifacts/parts.d.ts +34 -0
  49. package/dist/artifacts/parts.js +38 -0
  50. package/dist/artifacts/service.d.ts +74 -0
  51. package/dist/artifacts/service.js +301 -0
  52. package/dist/artifacts/workspace.d.ts +60 -0
  53. package/dist/artifacts/workspace.js +57 -0
  54. package/dist/artifacts/zip.d.ts +10 -0
  55. package/dist/artifacts/zip.js +126 -0
  56. package/dist/blob/fs.d.ts +13 -0
  57. package/dist/blob/fs.js +194 -0
  58. package/dist/blob/index.d.ts +9 -0
  59. package/dist/blob/index.js +9 -0
  60. package/dist/blob/s3.d.ts +42 -0
  61. package/dist/blob/s3.js +366 -0
  62. package/dist/blob/types.d.ts +108 -0
  63. package/dist/blob/types.js +85 -0
  64. package/dist/core/sandbox-routes.d.ts +2 -2
  65. package/dist/core/sandbox-routes.js +4 -3
  66. package/dist/execution/memory.d.ts +11 -1
  67. package/dist/execution/memory.js +77 -3
  68. package/dist/execution/types.d.ts +54 -1
  69. package/dist/execution/types.js +31 -0
  70. package/dist/gates/contract.d.ts +14 -13
  71. package/dist/gates/contract.js +13 -12
  72. package/dist/gates/egress.d.ts +72 -0
  73. package/dist/gates/egress.js +319 -0
  74. package/dist/gates/handler.d.ts +3 -0
  75. package/dist/gates/handler.js +3 -2
  76. package/dist/gates/http-client.d.ts +3 -2
  77. package/dist/gates/http-client.js +17 -6
  78. package/dist/gates/in-process.d.ts +13 -1
  79. package/dist/gates/in-process.js +11 -0
  80. package/dist/gates/inflight.d.ts +23 -4
  81. package/dist/gates/inflight.js +18 -4
  82. package/dist/gates/mcp-handler.js +5 -1
  83. package/dist/gates/model-gate.d.ts +6 -2
  84. package/dist/gates/tenant-vaults.d.ts +18 -9
  85. package/dist/gates/tenant-vaults.js +17 -12
  86. package/dist/gates/tool-calls.d.ts +9 -5
  87. package/dist/gates/tool-calls.js +6 -6
  88. package/dist/gates/tool-client.d.ts +9 -2
  89. package/dist/gates/tool-client.js +56 -22
  90. package/dist/gates/tool-contract.d.ts +12 -7
  91. package/dist/gates/tool-contract.js +10 -4
  92. package/dist/gates/tool-gate.d.ts +1 -0
  93. package/dist/harness/calls.d.ts +52 -0
  94. package/dist/harness/calls.js +113 -0
  95. package/dist/harness/executors.d.ts +22 -0
  96. package/dist/harness/executors.js +96 -0
  97. package/dist/harness/main.d.ts +6 -0
  98. package/dist/harness/main.js +103 -0
  99. package/dist/harness/pod.d.ts +21 -0
  100. package/dist/harness/pod.js +119 -0
  101. package/dist/harness/run-tokens.d.ts +18 -0
  102. package/dist/harness/run-tokens.js +15 -0
  103. package/dist/harness/service.d.ts +46 -0
  104. package/dist/harness/service.js +179 -0
  105. package/dist/harness/ws-client.d.ts +43 -0
  106. package/dist/harness/ws-client.js +122 -0
  107. package/dist/harness/ws-port.d.ts +22 -0
  108. package/dist/harness/ws-port.js +78 -0
  109. package/dist/harness-api/in-process.d.ts +24 -0
  110. package/dist/harness-api/in-process.js +119 -0
  111. package/dist/harness-api/record.d.ts +44 -0
  112. package/dist/harness-api/record.js +250 -0
  113. package/dist/harness-api/renew.d.ts +37 -0
  114. package/dist/harness-api/renew.js +77 -0
  115. package/dist/harness-api/server.d.ts +104 -0
  116. package/dist/harness-api/server.js +408 -0
  117. package/dist/harness-api/start.d.ts +36 -0
  118. package/dist/harness-api/start.js +118 -0
  119. package/dist/harness-api/workspace.d.ts +53 -0
  120. package/dist/harness-api/workspace.js +201 -0
  121. package/dist/harness-api/ws-server.d.ts +34 -0
  122. package/dist/harness-api/ws-server.js +227 -0
  123. package/dist/host/admin-api.d.ts +12 -5
  124. package/dist/host/admin-api.js +72 -6
  125. package/dist/host/app.d.ts +0 -1
  126. package/dist/host/app.js +35 -24
  127. package/dist/host/config-for.d.ts +4 -0
  128. package/dist/host/config-for.js +2 -0
  129. package/dist/host/config.d.ts +0 -6
  130. package/dist/host/create-host.d.ts +3 -10
  131. package/dist/host/create-host.js +4 -1
  132. package/dist/host/gates.d.ts +27 -2
  133. package/dist/host/gates.js +52 -2
  134. package/dist/host/http.d.ts +4 -1
  135. package/dist/host/http.js +6 -1
  136. package/dist/host/main.js +167 -37
  137. package/dist/host/stack-config.d.ts +112 -9
  138. package/dist/host/stack-config.js +222 -20
  139. package/dist/index.d.ts +3 -0
  140. package/dist/index.js +2 -0
  141. package/dist/keys/client.js +9 -1
  142. package/dist/keys/keys.d.ts +12 -2
  143. package/dist/keys/keys.js +8 -0
  144. package/dist/mcp/connect.js +2 -1
  145. package/dist/mcp/pool.d.ts +1 -0
  146. package/dist/mcp/pool.js +1 -0
  147. package/dist/mcp/snapshot.d.ts +8 -1
  148. package/dist/mcp/snapshot.js +28 -0
  149. package/dist/media.d.ts +5 -24
  150. package/dist/media.js +5 -0
  151. package/dist/model/pi-model.d.ts +6 -2
  152. package/dist/model/pi-model.js +39 -22
  153. package/dist/node/index.d.ts +1 -1
  154. package/dist/node/index.js +1 -1
  155. package/dist/openapi.json +14084 -9426
  156. package/dist/record/index.d.ts +1 -0
  157. package/dist/record/index.js +1 -0
  158. package/dist/record/sandbox.d.ts +25 -0
  159. package/dist/record/sandbox.js +43 -0
  160. package/dist/sandbox/capability.d.ts +3 -0
  161. package/dist/sandbox/capability.js +10 -0
  162. package/dist/sandbox/egress-token.d.ts +26 -0
  163. package/dist/sandbox/egress-token.js +8 -0
  164. package/dist/sandbox/join.d.ts +37 -0
  165. package/dist/sandbox/join.js +165 -0
  166. package/dist/sandbox/manager.d.ts +99 -34
  167. package/dist/sandbox/manager.js +216 -84
  168. package/dist/sandbox/placement.d.ts +27 -0
  169. package/dist/sandbox/placement.js +44 -0
  170. package/dist/sandbox/pods/client.d.ts +90 -0
  171. package/dist/sandbox/pods/client.js +79 -0
  172. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  173. package/dist/sandbox/pods/lifecycle.js +178 -0
  174. package/dist/sandbox/pods/name.d.ts +1 -0
  175. package/dist/sandbox/pods/name.js +28 -0
  176. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  177. package/dist/sandbox/pods/network-gate.js +71 -0
  178. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  179. package/dist/sandbox/pods/reconcile.js +152 -0
  180. package/dist/sandbox/pods/spec.d.ts +45 -0
  181. package/dist/sandbox/pods/spec.js +62 -0
  182. package/dist/sandbox/records.d.ts +29 -0
  183. package/dist/sandbox/records.js +79 -0
  184. package/dist/sandbox/resolve.d.ts +5 -0
  185. package/dist/sandbox/resolve.js +3 -2
  186. package/dist/sandbox/session-sandbox.js +7 -5
  187. package/dist/sandbox/share.d.ts +11 -0
  188. package/dist/sandbox/share.js +10 -0
  189. package/dist/sandbox/tenant-config.d.ts +16 -1
  190. package/dist/sandbox/tenant-config.js +9 -0
  191. package/dist/sandbox/types.d.ts +29 -1
  192. package/dist/sandbox/types.js +13 -1
  193. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  194. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  195. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  196. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  197. package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
  198. package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
  199. package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
  200. package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
  201. package/dist/store/postgres/record-writer.d.ts +6 -1
  202. package/dist/store/postgres/record-writer.js +20 -2
  203. package/dist/store/postgres/schema.d.ts +1168 -250
  204. package/dist/store/postgres/schema.js +174 -28
  205. package/dist/store/postgres/store.js +339 -103
  206. package/dist/store/postgres/tenant.d.ts +1 -1
  207. package/dist/store/postgres/tenant.js +2 -2
  208. package/dist/store/types.d.ts +163 -32
  209. package/dist/streams/control.d.ts +2 -5
  210. package/dist/streams/control.js +5 -8
  211. package/dist/streams/types.d.ts +8 -8
  212. package/dist/streams/types.js +1 -1
  213. package/dist/tenant/advance.js +218 -160
  214. package/dist/tenant/artifact-tool.d.ts +5 -0
  215. package/dist/tenant/artifact-tool.js +119 -0
  216. package/dist/tenant/auth.d.ts +13 -9
  217. package/dist/tenant/auth.js +25 -32
  218. package/dist/tenant/commands.js +31 -10
  219. package/dist/tenant/context.d.ts +83 -26
  220. package/dist/tenant/delivery-token.js +4 -4
  221. package/dist/tenant/effects.d.ts +24 -14
  222. package/dist/tenant/effects.js +10 -398
  223. package/dist/tenant/ephemeral.d.ts +14 -2
  224. package/dist/tenant/ephemeral.js +8 -1
  225. package/dist/tenant/history.d.ts +13 -23
  226. package/dist/tenant/history.js +14 -62
  227. package/dist/tenant/host-token.d.ts +92 -0
  228. package/dist/tenant/host-token.js +179 -0
  229. package/dist/tenant/identity-file.d.ts +75 -0
  230. package/dist/tenant/identity-file.js +202 -0
  231. package/dist/tenant/issuers.d.ts +48 -0
  232. package/dist/tenant/issuers.js +311 -0
  233. package/dist/tenant/jwt.js +3 -3
  234. package/dist/tenant/module.js +1 -0
  235. package/dist/tenant/operator-keys.d.ts +18 -0
  236. package/dist/tenant/operator-keys.js +55 -0
  237. package/dist/tenant/outbound.d.ts +30 -0
  238. package/dist/tenant/outbound.js +142 -10
  239. package/dist/tenant/paths.d.ts +2 -1
  240. package/dist/tenant/paths.js +3 -1
  241. package/dist/tenant/principals.d.ts +2 -11
  242. package/dist/tenant/principals.js +10 -30
  243. package/dist/tenant/reset.d.ts +5 -2
  244. package/dist/tenant/reset.js +22 -2
  245. package/dist/tenant/run-grants.d.ts +49 -0
  246. package/dist/tenant/run-grants.js +62 -0
  247. package/dist/tenant/run-token.d.ts +70 -0
  248. package/dist/tenant/run-token.js +138 -0
  249. package/dist/tenant/runtime.d.ts +42 -0
  250. package/dist/tenant/runtime.js +134 -33
  251. package/dist/tenant/sandboxes.d.ts +119 -0
  252. package/dist/tenant/sandboxes.js +551 -0
  253. package/dist/tenant/session-streams.d.ts +6 -10
  254. package/dist/tenant/session-streams.js +9 -27
  255. package/dist/tenant/session.d.ts +2 -13
  256. package/dist/tenant/session.js +4 -28
  257. package/dist/tenant/sessions.d.ts +6 -1
  258. package/dist/tenant/sessions.js +29 -2
  259. package/dist/tenant/signing-keys.d.ts +2 -1
  260. package/dist/tenant/signing-keys.js +2 -2
  261. package/dist/tenant/status.d.ts +8 -3
  262. package/dist/tenant/status.js +5 -0
  263. package/dist/tenant/streams.d.ts +8 -2
  264. package/dist/tenant/streams.js +25 -6
  265. package/dist/tenant/sweep.js +37 -4
  266. package/dist/tenant/types.d.ts +39 -1
  267. package/dist/tenant/worker.d.ts +11 -5
  268. package/dist/tenant/worker.js +12 -6
  269. package/dist/vault/crypto.d.ts +7 -0
  270. package/dist/vault/crypto.js +16 -0
  271. package/dist/vault/oauth.d.ts +55 -0
  272. package/dist/vault/oauth.js +186 -0
  273. package/dist/vault/service.d.ts +54 -7
  274. package/dist/vault/service.js +261 -17
  275. package/dist/vault/sources.d.ts +54 -0
  276. package/dist/vault/sources.js +185 -0
  277. package/dist/version.d.ts +1 -1
  278. package/dist/version.js +1 -1
  279. package/package.json +14 -10
  280. package/dist/adapters/media.d.ts +0 -24
  281. package/dist/adapters/media.js +0 -111
  282. package/dist/host/cors.d.ts +0 -27
  283. package/dist/host/cors.js +0 -64
  284. package/dist/tenant/access-policy.d.ts +0 -30
  285. package/dist/tenant/access-policy.js +0 -52
  286. package/dist/tenant/browser.d.ts +0 -15
  287. package/dist/tenant/browser.js +0 -39
  288. package/dist/tenant/subject-limits.d.ts +0 -16
  289. package/dist/tenant/subject-limits.js +0 -45
  290. package/dist/tenant/tokens.d.ts +0 -14
  291. package/dist/tenant/tokens.js +0 -180
@@ -14,10 +14,19 @@
14
14
  * - **Epoch-checked writes.** Every transaction of the advance and of the engine host
15
15
  * (`effects.ts`) runs through `ownedTx`/`ownedSession`, which checks the epoch first. A
16
16
  * mismatch throws `ownership.lost` with no write; the advance stops and returns `done`.
17
- * - **Heartbeat.** While the segment runs, a timer scoped to the advance renews the lease
18
- * (a column update). A failed renewal aborts the segment.
17
+ * - **Heartbeat.** While the segment runs the lease is renewed (a column update): by the
18
+ * harness that holds the run (`lease.renew`), and by core until one takes it. A failed
19
+ * renewal aborts the segment.
20
+ * - **Run token** (F5). Right after taking the lease the advance mints the run token its
21
+ * gate calls present (`run-grants.ts`); each renewal re-mints it before it expires, and it
22
+ * is dropped when the lease is lost or released. A harness gets it with its run.
19
23
  * - **Release** when the advance ends, whatever the outcome.
20
24
  *
25
+ * The segment runs in a harness (Harness API v1, `harness-api/`): the advance offers it with
26
+ * its `turn.start`, the in-process harness (or one attached to the Tenant) readies the
27
+ * session's MCP servers, runs the engine and reports how it ended, and the advance settles
28
+ * that exactly as it settled the engine's result.
29
+ *
21
30
  * Session outcomes never throw out of `advance`: a segment that throws is settled as a
22
31
  * failed turn. Only infrastructure errors (the Session Store is unreachable) throw, and the
23
32
  * execution retries them.
@@ -33,16 +42,17 @@
33
42
  import { runDurable, runFlowDurable, } from "@nylorun/harness/run";
34
43
  import { aggregateWaits, cancelSiblingWork, isWorkflowManifest, wakeLinkedWorkflow, } from "../core/flow-host.js";
35
44
  import { isOwnershipLost, ownedTx } from "../store/ownership.js";
36
- import { linkedAgentOutput, rebaseSessionState, sessionToolsOf, turnManifestOf, } from "./session.js";
37
- import { isRemoteMcpEffect, prepareMcp, recoversMcpCalls, recoversModelCalls, resolveEffect, } from "./effects.js";
45
+ import { linkedAgentOutput } from "./session.js";
46
+ import { isRemoteMcpEffect, recoversMcpCalls, recoversModelCalls } from "./effects.js";
38
47
  import { slimModelEffects } from "./slim.js";
39
- import { checkParity, foldTranscript, leanState, transcriptOf, transcriptUpdates, withTranscript, } from "./history.js";
48
+ import { applyUpdates, leanState, transcriptShadow, withTranscript, } from "./history.js";
40
49
  import { command } from "./commands.js";
41
- import { usesFixtureModel } from "./model-setting.js";
42
- import { toolFixtureModel } from "../core/provider.js";
50
+ import { exportOutputs } from "../artifacts/export.js";
51
+ import { dropRunGrant, grantRun } from "./run-grants.js";
52
+ import { startHeartbeat } from "../harness-api/renew.js";
53
+ import { buildTurnStart, startSegment } from "../harness-api/start.js";
54
+ import { touchPodSandbox } from "./sandboxes.js";
43
55
  import { AdvanceAbort, AdvanceDeadlineError, abortKind, } from "./worker.js";
44
- /** The model of Tenants with the fixture-model setting (`model-setting.ts`). Stateless. */
45
- const fixture = toolFixtureModel();
46
56
  /**
47
57
  * A segment stopped by a `shutdown` or `ownership.lost` abort before it settled. Never
48
58
  * leaves `advance`.
@@ -55,13 +65,39 @@ class SegmentStopped extends Error {
55
65
  this.kind = kind;
56
66
  }
57
67
  }
68
+ /**
69
+ * The harness holding the run went away without giving it back (`connection.lost`): an effect
70
+ * it was running may still be running, or may have died with it. Like a Worker that died, the
71
+ * advance keeps the lease, which lapses, so the next advance takes the session over and marks
72
+ * what was `invoking` `uncertain` (§11.4). Never leaves `advance`.
73
+ */
74
+ class RunLost extends Error {
75
+ name = "RunLost";
76
+ constructor() {
77
+ super("The harness holding the run went away");
78
+ }
79
+ }
58
80
  /** Throws `SegmentStopped` when the signal aborted for a reason that must not settle. */
59
81
  function stopIfLeaving(signal) {
60
82
  const kind = abortKind(signal);
61
83
  if (kind === "shutdown" || kind === "ownership.lost")
62
84
  throw new SegmentStopped(kind);
63
85
  }
86
+ /** No harness took the segment (`HarnessApiServer.offer`): the advance is retried later. */
87
+ class HarnessUnavailable extends Error {
88
+ name = "HarnessUnavailable";
89
+ constructor() {
90
+ super("No harness took the segment");
91
+ }
92
+ }
64
93
  const DONE = { status: "done" };
94
+ /** How soon an advance whose segment no harness took is tried again. */
95
+ const UNAVAILABLE_RETRY_MS = 1000;
96
+ /**
97
+ * How long a pod sandbox's segment waits for its engine (F7.2): a pod that is starting, or
98
+ * resuming, takes longer than a harness that is already connected.
99
+ */
100
+ const POD_OFFER_WAIT_MS = 60_000;
65
101
  /** Shortest `busy` retry, so a lease about to lapse is not polled in a tight loop. */
66
102
  const MIN_RETRY_MS = 25;
67
103
  /**
@@ -88,13 +124,33 @@ export async function advance(ctx, id, signal) {
88
124
  signal.addEventListener("abort", forward, { once: true });
89
125
  ctx.work.running.set(id, controller);
90
126
  ctx.work.runningTurns.set(id, taken.session.activeTurnId ?? null);
91
- const heartbeat = startHeartbeat(ctx, lease, controller);
127
+ // A harness renews the lease once it holds the run; until then, and without one, core does.
128
+ const run = { agentId: taken.session.agentId, activeTurnId: taken.session.activeTurnId };
129
+ const heartbeat = startHeartbeat(ctx, lease, controller, run);
92
130
  let result = DONE;
131
+ let release = true;
132
+ const placed = {};
93
133
  try {
94
- await runSegment(ctx, lease, taken.session, controller.signal);
134
+ await grantRun(ctx, lease, run);
135
+ await runRemoteSegment(ctx, lease, taken.session, controller, run, () => heartbeat.stop(), placed);
95
136
  }
96
137
  catch (error) {
97
- if (error instanceof SegmentStopped && error.kind === "shutdown") {
138
+ if (error instanceof RunLost) {
139
+ ctx.config.logger.warn("advance lost its harness; the lease lapses and the session is taken over", {
140
+ sessionId: id,
141
+ epoch: lease.epoch,
142
+ });
143
+ release = false;
144
+ result = { status: "busy", retryAfterMs: ctx.ownerLeaseMs };
145
+ }
146
+ else if (error instanceof HarnessUnavailable) {
147
+ ctx.config.logger.warn("advance found no harness; session left for the next advance", {
148
+ sessionId: id,
149
+ epoch: lease.epoch,
150
+ });
151
+ result = { status: "busy", retryAfterMs: UNAVAILABLE_RETRY_MS };
152
+ }
153
+ else if (error instanceof SegmentStopped && error.kind === "shutdown") {
98
154
  // Left for the next advance, which resumes from the checkpoint once this one releases.
99
155
  ctx.config.logger.info("advance stopped for shutdown; session left for the next advance", {
100
156
  sessionId: id,
@@ -112,21 +168,44 @@ export async function advance(ctx, id, signal) {
112
168
  }
113
169
  finally {
114
170
  heartbeat.stop();
171
+ dropRunGrant(ctx, lease);
115
172
  signal.removeEventListener("abort", forward);
116
173
  if (ctx.work.running.get(id) === controller) {
117
174
  ctx.work.running.delete(id);
118
175
  ctx.work.runningTurns.delete(id);
119
176
  }
120
177
  // Best effort: a release that fails leaves a lease that simply expires.
121
- await ctx.store
122
- .tx((t) => t.releaseOwnership(id, lease.owner, lease.epoch))
123
- .catch((error) => ctx.config.logger.warn("advance failed to release ownership", {
124
- sessionId: id,
125
- message: error instanceof Error ? error.message : String(error),
126
- }));
178
+ if (release)
179
+ await ctx.store
180
+ .tx((t) => t.releaseOwnership(id, lease.owner, lease.epoch))
181
+ .catch((error) => ctx.config.logger.warn("advance failed to release ownership", {
182
+ sessionId: id,
183
+ message: error instanceof Error ? error.message : String(error),
184
+ }));
185
+ // A pod sandbox's idle time counts from the end of each segment (D34).
186
+ if (placed.pod !== undefined)
187
+ await touchPodSandbox(ctx, placed.pod);
127
188
  }
128
189
  return result;
129
190
  }
191
+ /** Why a pod sandbox cannot run a segment, or undefined when it can. */
192
+ function podUnusable(ctx, segment) {
193
+ const pod = segment.pod;
194
+ if (!pod)
195
+ return undefined;
196
+ if (!ctx.pods)
197
+ return `Sandbox ${pod.id} is a pod sandbox, and this Runtime has no sandbox pods`;
198
+ const state = pod.pod;
199
+ if (state.desired === "deleted")
200
+ return `Sandbox ${pod.id} was deleted`;
201
+ if (state.observed === "lost")
202
+ return `Sandbox ${pod.id} was lost: ${state.reason ?? "its volume is gone"}`;
203
+ if (state.observed === "expired")
204
+ return `Sandbox ${pod.id} expired`;
205
+ if (state.observed === "failed")
206
+ return `Sandbox ${pod.id} failed to start: ${state.reason ?? "its pod was not ready"}`;
207
+ return undefined;
208
+ }
130
209
  /**
131
210
  * Takes ownership of `id` in one transaction, takes over from a dead owner, and marks the
132
211
  * session `running` when it has a runnable checkpoint. Releases again when there is
@@ -195,122 +274,66 @@ export async function takeOver(t, s, options = {}) {
195
274
  return marked;
196
275
  }
197
276
  /**
198
- * Renews the lease every third of its length while the advance runs; aborts it when lost.
199
- * Stops renewing once the advance is aborted (cancel, deadline, Worker stop): an advance that
200
- * does not wind down within the lease is taken over when it lapses (`worker.ts`).
277
+ * Runs one segment in a harness and settles it. Throws only `ownership.lost`, `SegmentStopped`,
278
+ * `HarnessUnavailable` and infrastructure errors. `onTaken` runs when a harness takes the run.
201
279
  */
202
- function startHeartbeat(ctx, lease, controller) {
203
- const every = Math.max(10, Math.floor(ctx.ownerLeaseMs / 3));
204
- let stopped = false;
205
- let timer;
206
- const beat = async () => {
207
- if (controller.signal.aborted) {
208
- stopped = true;
209
- return;
210
- }
211
- try {
212
- const renewed = await ctx.store.tx((t) => t.renewOwnership(lease.sessionId, lease.owner, lease.epoch, new Date(Date.now() + ctx.ownerLeaseMs)));
213
- if (!renewed && !stopped) {
214
- stopped = true;
215
- controller.abort(new AdvanceAbort("ownership.lost", "Ownership lost"));
216
- return;
217
- }
218
- }
219
- catch (error) {
220
- if (!stopped)
221
- ctx.config.logger.warn("advance heartbeat failed", {
222
- sessionId: lease.sessionId,
223
- message: error instanceof Error ? error.message : String(error),
224
- });
225
- }
226
- arm();
227
- };
228
- const arm = () => {
229
- if (stopped)
230
- return;
231
- timer = setTimeout(() => void beat(), every);
232
- timer.unref();
233
- };
234
- arm();
235
- return {
236
- stop() {
237
- stopped = true;
238
- clearTimeout(timer);
239
- },
240
- };
241
- }
242
- /** Segment rollover defaults (Model Calls §10), well inside the advance deadline. */
243
- const ROLLOVER_STEPS = 50;
244
- const ROLLOVER_MS = 20 * 60_000;
245
- /** Runs one segment and settles it. Throws only `ownership.lost` and infrastructure errors. */
246
- async function runSegment(ctx, lease, started, signal) {
247
- const id = lease.sessionId;
280
+ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, placed = {}) {
281
+ const { signal } = controller;
282
+ let end;
248
283
  try {
249
- // prepareMcp mutates the session's mcpSnapshot; read current after it.
250
- if (!isWorkflowManifest(started.manifest))
251
- await prepareMcp(ctx, lease, signal);
252
- const { current, fixtureModel } = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
253
- if (!isWorkflowManifest(current.manifest) && current.checkpoint) {
254
- rebaseSessionState(current, current.checkpoint.manifestHash);
255
- await adoptStoredTranscript(t, current);
256
- const cp = current.checkpoint;
257
- current.checkpoint = { ...cp, state: current.state };
258
- await t.put("sessions", id, current);
259
- }
260
- return { current, fixtureModel: await usesFixtureModel(t) };
284
+ const segment = await startSegment(ctx, lease, { harness: true });
285
+ // A pod sandbox's segment runs in its pod (F7.2), or fails when the pod cannot run it.
286
+ const unusable = podUnusable(ctx, segment);
287
+ if (unusable)
288
+ throw new Error(unusable);
289
+ const pod = segment.pod?.id;
290
+ if (pod !== undefined)
291
+ placed.pod = pod;
292
+ end = await ctx.harness.offer({
293
+ lease,
294
+ start: buildTurnStart(ctx, segment),
295
+ controller,
296
+ transcript: segment.transcript,
297
+ run,
298
+ onTaken,
299
+ ...(pod === undefined ? {} : { pod, waitMs: POD_OFFER_WAIT_MS }),
261
300
  });
262
- // One history (P0.3): the engine resumes from the transcript folded from the record.
263
- let startTranscript = [];
264
- let engineCheckpoint = current.checkpoint;
265
- if (!isWorkflowManifest(current.manifest) && current.checkpoint) {
266
- const cp = current.checkpoint;
267
- if (cp.state) {
268
- startTranscript = await foldSession(ctx, current);
269
- checkParity(id, startTranscript, transcriptOf(cp.state));
270
- engineCheckpoint = { ...cp, state: withTranscript(cp.state, startTranscript) };
271
- }
301
+ if (end.kind === "unavailable") {
302
+ // A pod stopped, or not yet started: make sure it is asked to run.
303
+ if (pod !== undefined)
304
+ await ctx.sandboxSignal(pod, { kind: "reconcile" }).catch(() => undefined);
305
+ throw new HarnessUnavailable();
272
306
  }
273
- const segment = fixtureModel ? { model: fixture } : {};
274
- const host = {
275
- resolveEffect: (e) => resolveEffect(ctx, e, signal, lease, segment),
276
- };
277
- const result = isWorkflowManifest(current.manifest)
278
- ? await runFlowDurable({
279
- manifest: current.manifest,
280
- checkpoint: current.checkpoint,
281
- signal,
282
- host,
283
- limits: ctx.flowLimits,
284
- })
285
- : await runDurable({
286
- manifest: turnManifestOf(current),
287
- checkpoint: engineCheckpoint,
288
- signal,
289
- sessionTools: sessionToolsOf(current.mcpSnapshot),
290
- host,
291
- yieldAfter: {
292
- steps: ctx.config.rollover?.steps ?? ROLLOVER_STEPS,
293
- ms: ctx.config.rollover?.ms ?? ROLLOVER_MS,
294
- },
295
- });
296
- // The engine turns an abort into a `cancelled` (agents) or `failed` (workflows) result;
297
- // only a user cancel may settle that, and it already did.
307
+ // Aborted before a harness took it: settled as an abort of the engine would be.
308
+ if (end.kind === "aborted")
309
+ throw signal.reason;
310
+ // Checked before the abort: whatever core's signal says, the run's effects are unaccounted for.
311
+ if (end.kind === "released" && end.reason === "connection.lost")
312
+ throw new RunLost();
298
313
  stopIfLeaving(signal);
314
+ if (end.kind === "released")
315
+ throw new SegmentStopped(end.reason === "ownership.lost" ? "ownership.lost" : "shutdown");
316
+ const { output } = end;
317
+ if (output.thrown)
318
+ throw thrownError(output.thrown);
319
+ const result = resultOf(output, segment);
320
+ let cursor;
299
321
  if (signal.reason instanceof AdvanceDeadlineError &&
300
322
  (result.status === "cancelled" || result.status === "failed"))
301
323
  await settleFailure(ctx, lease, started, signal.reason);
302
324
  else {
303
- const state = !isWorkflowManifest(current.manifest) &&
304
- (result.status === "yielded" || result.status === "completed" || result.status === "paused") &&
305
- "result" in result
306
- ? result.result?.state
307
- : undefined;
308
- const updates = state ? transcriptUpdates(startTranscript, transcriptOf(state)) : [];
309
- await settle(ctx, lease, started, result, updates);
325
+ const recorded = result.status === "yielded" || result.status === "completed" || result.status === "paused";
326
+ cursor = await settleTurn(ctx, lease, started, result, recorded ? output.transcript ?? [] : [], signal, segment.cursor);
310
327
  }
328
+ end.reply(cursor === undefined ? {} : { cursor });
311
329
  }
312
330
  catch (error) {
313
- if (isOwnershipLost(error) || error instanceof SegmentStopped)
331
+ if (end?.kind === "output")
332
+ end.refuse(error);
333
+ if (isOwnershipLost(error) ||
334
+ error instanceof SegmentStopped ||
335
+ error instanceof HarnessUnavailable ||
336
+ error instanceof RunLost)
314
337
  throw error;
315
338
  stopIfLeaving(signal);
316
339
  // A segment stopped by its deadline fails with the deadline, not the abort it caused.
@@ -318,6 +341,47 @@ async function runSegment(ctx, lease, started, signal) {
318
341
  await settleFailure(ctx, lease, started, deadline ? signal.reason : error);
319
342
  }
320
343
  }
344
+ /** The error an engine threw in the harness, as it would have been thrown here. */
345
+ function thrownError(thrown) {
346
+ const error = new Error(thrown.message);
347
+ // A lost epoch stops the segment without a write, wherever it was noticed.
348
+ return thrown.code === "ownership_lost"
349
+ ? Object.assign(error, { code: "ownership.lost" })
350
+ : error;
351
+ }
352
+ /**
353
+ * A harness's output as the engine's result, so `settle` is unchanged. The checkpoint is the
354
+ * segment's own (with the agent's new state); in shadow mode the state gets back the transcript
355
+ * the harness edited.
356
+ */
357
+ function resultOf(output, segment) {
358
+ const { current } = segment;
359
+ const status = output.status ?? "failed";
360
+ if (status === "waiting" || status === "uncertain")
361
+ return {
362
+ status,
363
+ checkpoint: current.checkpoint,
364
+ effectIds: output.effectIds ?? [],
365
+ };
366
+ let state = output.state;
367
+ if (state !== undefined && transcriptShadow())
368
+ state = withTranscript(state, applyUpdates(segment.transcript, output.transcript ?? []));
369
+ const checkpoint = state === undefined || isWorkflowManifest(current.manifest)
370
+ ? current.checkpoint
371
+ : { ...current.checkpoint, state };
372
+ return {
373
+ status,
374
+ checkpoint,
375
+ result: {
376
+ status,
377
+ ...(state === undefined ? {} : { state }),
378
+ ...(output.output === undefined ? {} : { output: output.output }),
379
+ ...(output.pending === undefined ? {} : { pending: output.pending }),
380
+ ...(output.error === undefined ? {} : { error: output.error }),
381
+ },
382
+ ...(output.cancelEffectIds ? { cancelEffectIds: output.cancelEffectIds } : {}),
383
+ };
384
+ }
321
385
  /** Wake the workflow a linked agent belongs to, in the agent's settle transaction. */
322
386
  async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
323
387
  if (type === "turn.completed" || type === "turn.failed")
@@ -344,9 +408,25 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
344
408
  schedule: ctx.wake,
345
409
  });
346
410
  }
347
- /** Record a segment result, then cancel failed siblings and wake a linked workflow. */
348
- async function settle(ctx, lease, s, result, updates = []) {
411
+ /**
412
+ * `settle`, then, for a completed turn, the turn-end export of its sandbox outputs (F8.2): after
413
+ * the turn has committed and while the advance still holds the lease. The harness hears back
414
+ * after the export. The export never throws.
415
+ */
416
+ async function settleTurn(ctx, lease, s, result, updates, signal, cursor) {
417
+ const kept = await settle(ctx, lease, s, result, updates, cursor);
418
+ if (result.status === "completed")
419
+ await exportOutputs(ctx, lease.sessionId, s.activeTurnId, signal);
420
+ return kept;
421
+ }
422
+ /**
423
+ * Record a segment result, then cancel failed siblings and wake a linked workflow. Returns the
424
+ * cursor of the transcript it recorded (`cursor` when it recorded no edit), or undefined when
425
+ * the transcript was not kept.
426
+ */
427
+ async function settle(ctx, lease, s, result, updates = [], cursor) {
349
428
  const id = lease.sessionId;
429
+ let kept;
350
430
  const siblingCancelIds = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
351
431
  const siblings = [];
352
432
  if (current.status === "cancelled" ||
@@ -381,7 +461,7 @@ async function settle(ctx, lease, s, result, updates = []) {
381
461
  if (result.status === "yielded") {
382
462
  // The turn goes on in a new segment: same turn, next checkpoint, woken right away.
383
463
  const finished = result.checkpoint;
384
- await recordTranscript(t, id, s.activeTurnId, current, updates);
464
+ kept = (await recordTranscript(t, id, s.activeTurnId, current, updates)) ?? cursor;
385
465
  current.state = leanState(result.result.state);
386
466
  current.checkpoint = {
387
467
  ...finished,
@@ -402,8 +482,11 @@ async function settle(ctx, lease, s, result, updates = []) {
402
482
  }
403
483
  const flow = isWorkflowManifest(current.manifest);
404
484
  if (!flow) {
405
- if (result.status !== "failed")
406
- await recordTranscript(t, id, s.activeTurnId, current, updates);
485
+ if (result.status !== "failed") {
486
+ const last = await recordTranscript(t, id, s.activeTurnId, current, updates);
487
+ if (result.status !== "cancelled")
488
+ kept = last ?? cursor;
489
+ }
407
490
  current.state =
408
491
  result.status === "failed"
409
492
  ? current.turnStartState
@@ -480,6 +563,7 @@ async function settle(ctx, lease, s, result, updates = []) {
480
563
  /* agent may already be terminal */
481
564
  }
482
565
  }
566
+ return kept;
483
567
  }
484
568
  /** A segment threw: fail the turn, restore the turn-start state, wake a linked workflow. */
485
569
  async function settleFailure(ctx, lease, s, error) {
@@ -508,43 +592,17 @@ function leanCheckpoint(checkpoint) {
508
592
  return checkpoint;
509
593
  return { ...checkpoint, state: leanState(state) };
510
594
  }
511
- /** Appends a segment's transcript edits; a snapshot moves `history.snapshot`. */
595
+ /**
596
+ * Appends a segment's transcript edits; a snapshot moves `history.snapshot`. Returns the seq of
597
+ * the last edit, if any.
598
+ */
512
599
  async function recordTranscript(t, id, turnId, s, updates) {
600
+ let last;
513
601
  for (const update of updates) {
514
602
  const event = await t.event(id, turnId, "transcript.updated", update);
515
603
  if (update.keep === 0)
516
604
  s.history = { from: s.history?.from ?? event.seq, snapshot: event.seq };
605
+ last = event.seq;
517
606
  }
518
- }
519
- /**
520
- * A session written before transcripts were recorded keeps its transcript on its row: record
521
- * it once (the turn's starting transcript, then the turn so far), and store the row lean.
522
- */
523
- async function adoptStoredTranscript(t, s) {
524
- if (s.history)
525
- return;
526
- const now = transcriptOf(s.state);
527
- const start = s.activeTurnId ? transcriptOf(s.turnStartState) : now;
528
- if (now.length === 0 && start.length === 0)
529
- return;
530
- let first;
531
- const write = async (turnId, updates) => {
532
- for (const update of updates) {
533
- const event = await t.event(s.id, turnId, "transcript.updated", update);
534
- first ??= event.seq;
535
- }
536
- };
537
- await write(null, transcriptUpdates([], start));
538
- if (s.activeTurnId)
539
- await write(s.activeTurnId, transcriptUpdates(start, now));
540
- s.history = { from: first ?? 0, snapshot: first ?? 0 };
541
- s.state = leanState(s.state);
542
- s.turnStartState = leanState(s.turnStartState);
543
- }
544
- /** The session's transcript, folded from its record from `history.from`. */
545
- async function foldSession(ctx, s) {
546
- const rows = await ctx.store
547
- .record()
548
- .readRange(ctx.store.tenantId, s.id, s.history?.from ?? 0, Number.MAX_SAFE_INTEGER);
549
- return foldTranscript(rows.map((row) => row.body));
607
+ return last;
550
608
  }
@@ -0,0 +1,5 @@
1
+ import type { SandboxToolOutcome } from "@nylorun/core/contracts";
2
+ import type { HostEffect } from "@nylorun/harness/run";
3
+ import { type TenantContext } from "./context.js";
4
+ export { isSaveArtifactCall } from "../harness/calls.js";
5
+ export declare function callSaveArtifact(ctx: TenantContext, request: HostEffect, signal: AbortSignal): Promise<SandboxToolOutcome>;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `nylorun.save_artifact` (F8.1): our engine's built-in tool that saves a file as an artifact
3
+ * of its session. The `nylorun.artifacts` capability comes with a session's sandbox
4
+ * (`sandbox/session-sandbox.ts`); the Runtime runs the tool itself, like the sandbox tools:
5
+ *
6
+ * - `path`: a file in the session's sandbox, read as bytes in the sandbox's queue;
7
+ * - `content`: text the model passes.
8
+ *
9
+ * The version commits with `artifact.created` (or `artifact.version.created`) on the session's
10
+ * stream, carrying the turn and the tool call. Expected problems (no such file, too large, the
11
+ * Tenant's total reached) are failed outcomes the model sees; only infrastructure errors throw.
12
+ */
13
+ import { basename } from "node:path";
14
+ import { SANDBOX_CAPABILITY_ID } from "@nylorun/core/define";
15
+ import { isArtifactId } from "@nylorun/core/compatibility";
16
+ import { readArtifactLimits } from "../artifacts/config.js";
17
+ import { contentTypeFor, normalizeContentType } from "../artifacts/media-types.js";
18
+ import { uploadArtifact } from "../artifacts/service.js";
19
+ import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
20
+ import { sandboxCapabilityOf } from "../sandbox/manager.js";
21
+ import { sandboxWorkspaceOf } from "../sandbox/share.js";
22
+ import { sandboxLookup, sessionOf } from "./context.js";
23
+ import { HttpError } from "./http.js";
24
+ import { manifestFor } from "./session.js";
25
+ import { toolIds } from "./transcript.js";
26
+ export { isSaveArtifactCall } from "../harness/calls.js";
27
+ const failed = (code, message) => ({
28
+ kind: "failed",
29
+ code,
30
+ message,
31
+ });
32
+ export async function callSaveArtifact(ctx, request, signal) {
33
+ const input = (request.input ?? {});
34
+ const path = typeof input.path === "string" && input.path !== "" ? input.path : undefined;
35
+ const content = typeof input.content === "string" ? input.content : undefined;
36
+ if ((path === undefined) === (content === undefined))
37
+ return failed("artifact.invalid_input", "Give path or content, not both.");
38
+ const artifactId = typeof input.artifactId === "string" ? input.artifactId : undefined;
39
+ const { s, workspace, limits, ours } = await ctx.store.tx(async (t) => {
40
+ const s = await sessionOf(t, request.sessionId);
41
+ const lookup = await sandboxLookup(t, s.id);
42
+ // A new version only of an artifact of this session.
43
+ const ours = artifactId === undefined ||
44
+ (isArtifactId(artifactId) && (await t.artifact(artifactId))?.sessionId === s.id);
45
+ return {
46
+ s,
47
+ workspace: sandboxWorkspaceOf(s, lookup),
48
+ limits: await readArtifactLimits(t),
49
+ ours,
50
+ };
51
+ });
52
+ if (!ours)
53
+ return failed("artifact.not_found", `Artifact ${artifactId} is not an artifact of this session.`);
54
+ let bytes;
55
+ let fileName;
56
+ if (path !== undefined) {
57
+ const capability = sandboxCapabilityOf(manifestFor(s.manifest, request.agent), SANDBOX_CAPABILITY_ID, "read");
58
+ if (!capability)
59
+ return failed("artifact.no_sandbox", "This session has no sandbox to read a path from.");
60
+ const read = await ctx.sandbox.readBytes({
61
+ id: workspace.ownerId,
62
+ activeTurnId: s.activeTurnId,
63
+ manifest: s.manifest,
64
+ ...(workspace.sandboxId === undefined ? {} : { sandboxId: workspace.sandboxId }),
65
+ }, capability, path, limits.fileBytes, signal);
66
+ if (read.kind === "failed")
67
+ return read;
68
+ if (read.kind === "missing")
69
+ return failed("artifact.not_found", `${read.path} does not exist in the sandbox.`);
70
+ bytes = read.bytes;
71
+ fileName = basename(read.path);
72
+ }
73
+ else {
74
+ bytes = new TextEncoder().encode(content);
75
+ fileName = "artifact.txt";
76
+ }
77
+ const name = typeof input.name === "string" && input.name !== "" ? input.name : fileName;
78
+ let contentType;
79
+ if (typeof input.contentType === "string") {
80
+ const declared = normalizeContentType(input.contentType);
81
+ if (declared === undefined)
82
+ return failed("artifact.invalid_input", `${input.contentType} is not a media type.`);
83
+ contentType = declared;
84
+ }
85
+ else {
86
+ contentType = contentTypeFor(name);
87
+ if (content !== undefined && contentType === DEFAULT_CONTENT_TYPE)
88
+ contentType = "text/plain; charset=utf-8";
89
+ }
90
+ const ids = toolIds(request.context);
91
+ try {
92
+ const saved = await uploadArtifact(ctx, {
93
+ ...(artifactId !== undefined ? { artifactId } : { name }),
94
+ contentType,
95
+ sessionId: s.id,
96
+ source: "engine",
97
+ turnId: request.turnId,
98
+ ...("callId" in ids ? { callId: ids.callId } : {}),
99
+ }, bytes,
100
+ // Our engine saves into its own session only.
101
+ { owner: s.ownerUserId }, signal);
102
+ return {
103
+ kind: "completed",
104
+ output: {
105
+ artifactId: saved.artifact.artifactId,
106
+ version: saved.version.version,
107
+ name: saved.artifact.name,
108
+ contentType: saved.version.contentType,
109
+ size: saved.version.size,
110
+ sha256: saved.version.sha256,
111
+ },
112
+ };
113
+ }
114
+ catch (error) {
115
+ if (error instanceof HttpError)
116
+ return failed(error.rejection.code === "limit_exceeded" ? "artifact.too_large" : "artifact.rejected", error.message);
117
+ throw error;
118
+ }
119
+ }