@nylorun/runtime 0.15.1-beta → 0.16.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 (220) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +33 -6
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/replication/pgoutput.js +3 -1
  6. package/dist/adapters/sandbox/local.d.ts +10 -0
  7. package/dist/adapters/sandbox/local.js +195 -0
  8. package/dist/adapters/sandbox/virtual.js +36 -1
  9. package/dist/admin-openapi.json +37 -3
  10. package/dist/api/a2a/tasks.js +6 -1
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.js +5 -1
  13. package/dist/api/components.d.ts +2673 -655
  14. package/dist/api/components.js +17 -1
  15. package/dist/api/gate/keys.d.ts +4 -3
  16. package/dist/api/gate/routes.d.ts +19 -3
  17. package/dist/api/gate/routes.js +162 -53
  18. package/dist/api/http/app.js +4 -0
  19. package/dist/api/http/define.d.ts +10 -0
  20. package/dist/api/http/define.js +2 -1
  21. package/dist/api/http/routes/artifacts.d.ts +11 -0
  22. package/dist/api/http/routes/artifacts.js +518 -0
  23. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  24. package/dist/api/http/routes/sandboxes.js +152 -0
  25. package/dist/api/http/routes/sessions.js +3 -1
  26. package/dist/api/http/routes/tenant.js +36 -2
  27. package/dist/artifacts/config.d.ts +23 -0
  28. package/dist/artifacts/config.js +27 -0
  29. package/dist/artifacts/export.d.ts +8 -0
  30. package/dist/artifacts/export.js +205 -0
  31. package/dist/artifacts/files.d.ts +36 -0
  32. package/dist/artifacts/files.js +45 -0
  33. package/dist/artifacts/folders.d.ts +60 -0
  34. package/dist/artifacts/folders.js +209 -0
  35. package/dist/artifacts/links.d.ts +28 -0
  36. package/dist/artifacts/links.js +92 -0
  37. package/dist/artifacts/media-types.d.ts +13 -0
  38. package/dist/artifacts/media-types.js +80 -0
  39. package/dist/artifacts/parts.d.ts +34 -0
  40. package/dist/artifacts/parts.js +38 -0
  41. package/dist/artifacts/service.d.ts +74 -0
  42. package/dist/artifacts/service.js +301 -0
  43. package/dist/artifacts/workspace.d.ts +60 -0
  44. package/dist/artifacts/workspace.js +57 -0
  45. package/dist/artifacts/zip.d.ts +10 -0
  46. package/dist/artifacts/zip.js +126 -0
  47. package/dist/blob/fs.d.ts +13 -0
  48. package/dist/blob/fs.js +194 -0
  49. package/dist/blob/index.d.ts +9 -0
  50. package/dist/blob/index.js +9 -0
  51. package/dist/blob/s3.d.ts +42 -0
  52. package/dist/blob/s3.js +366 -0
  53. package/dist/blob/types.d.ts +108 -0
  54. package/dist/blob/types.js +85 -0
  55. package/dist/core/sandbox-routes.d.ts +2 -2
  56. package/dist/core/sandbox-routes.js +4 -3
  57. package/dist/execution/memory.d.ts +11 -1
  58. package/dist/execution/memory.js +77 -3
  59. package/dist/execution/types.d.ts +54 -1
  60. package/dist/execution/types.js +31 -0
  61. package/dist/gates/contract.d.ts +14 -13
  62. package/dist/gates/contract.js +13 -12
  63. package/dist/gates/egress.d.ts +72 -0
  64. package/dist/gates/egress.js +319 -0
  65. package/dist/gates/handler.d.ts +3 -0
  66. package/dist/gates/handler.js +3 -2
  67. package/dist/gates/http-client.d.ts +3 -2
  68. package/dist/gates/http-client.js +17 -6
  69. package/dist/gates/in-process.d.ts +13 -1
  70. package/dist/gates/in-process.js +11 -0
  71. package/dist/gates/inflight.d.ts +23 -4
  72. package/dist/gates/inflight.js +18 -4
  73. package/dist/gates/model-gate.d.ts +6 -2
  74. package/dist/gates/tool-calls.d.ts +9 -5
  75. package/dist/gates/tool-calls.js +6 -6
  76. package/dist/gates/tool-client.d.ts +9 -2
  77. package/dist/gates/tool-client.js +56 -22
  78. package/dist/gates/tool-contract.d.ts +12 -7
  79. package/dist/gates/tool-contract.js +10 -4
  80. package/dist/gates/tool-gate.d.ts +1 -0
  81. package/dist/harness/calls.d.ts +52 -0
  82. package/dist/harness/calls.js +113 -0
  83. package/dist/harness/executors.d.ts +22 -0
  84. package/dist/harness/executors.js +96 -0
  85. package/dist/harness/main.d.ts +6 -0
  86. package/dist/harness/main.js +103 -0
  87. package/dist/harness/pod.d.ts +21 -0
  88. package/dist/harness/pod.js +119 -0
  89. package/dist/harness/run-tokens.d.ts +18 -0
  90. package/dist/harness/run-tokens.js +15 -0
  91. package/dist/harness/service.d.ts +46 -0
  92. package/dist/harness/service.js +179 -0
  93. package/dist/harness/ws-client.d.ts +43 -0
  94. package/dist/harness/ws-client.js +122 -0
  95. package/dist/harness/ws-port.d.ts +22 -0
  96. package/dist/harness/ws-port.js +78 -0
  97. package/dist/harness-api/in-process.d.ts +24 -0
  98. package/dist/harness-api/in-process.js +119 -0
  99. package/dist/harness-api/record.d.ts +44 -0
  100. package/dist/harness-api/record.js +250 -0
  101. package/dist/harness-api/renew.d.ts +37 -0
  102. package/dist/harness-api/renew.js +77 -0
  103. package/dist/harness-api/server.d.ts +104 -0
  104. package/dist/harness-api/server.js +408 -0
  105. package/dist/harness-api/start.d.ts +36 -0
  106. package/dist/harness-api/start.js +118 -0
  107. package/dist/harness-api/workspace.d.ts +53 -0
  108. package/dist/harness-api/workspace.js +201 -0
  109. package/dist/harness-api/ws-server.d.ts +34 -0
  110. package/dist/harness-api/ws-server.js +227 -0
  111. package/dist/host/app.js +23 -2
  112. package/dist/host/gates.d.ts +27 -2
  113. package/dist/host/gates.js +46 -1
  114. package/dist/host/http.d.ts +4 -1
  115. package/dist/host/http.js +6 -1
  116. package/dist/host/main.js +135 -27
  117. package/dist/host/stack-config.d.ts +101 -1
  118. package/dist/host/stack-config.js +199 -8
  119. package/dist/mcp/snapshot.d.ts +8 -1
  120. package/dist/mcp/snapshot.js +28 -0
  121. package/dist/media.d.ts +5 -24
  122. package/dist/media.js +5 -0
  123. package/dist/model/pi-model.d.ts +6 -2
  124. package/dist/model/pi-model.js +39 -22
  125. package/dist/node/index.d.ts +1 -1
  126. package/dist/node/index.js +1 -1
  127. package/dist/openapi.json +15245 -8850
  128. package/dist/record/index.d.ts +1 -0
  129. package/dist/record/index.js +1 -0
  130. package/dist/record/sandbox.d.ts +25 -0
  131. package/dist/record/sandbox.js +43 -0
  132. package/dist/sandbox/capability.d.ts +3 -0
  133. package/dist/sandbox/capability.js +10 -0
  134. package/dist/sandbox/egress-token.d.ts +26 -0
  135. package/dist/sandbox/egress-token.js +8 -0
  136. package/dist/sandbox/join.d.ts +37 -0
  137. package/dist/sandbox/join.js +165 -0
  138. package/dist/sandbox/manager.d.ts +99 -34
  139. package/dist/sandbox/manager.js +216 -84
  140. package/dist/sandbox/placement.d.ts +27 -0
  141. package/dist/sandbox/placement.js +44 -0
  142. package/dist/sandbox/pods/client.d.ts +90 -0
  143. package/dist/sandbox/pods/client.js +79 -0
  144. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  145. package/dist/sandbox/pods/lifecycle.js +178 -0
  146. package/dist/sandbox/pods/name.d.ts +1 -0
  147. package/dist/sandbox/pods/name.js +28 -0
  148. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  149. package/dist/sandbox/pods/network-gate.js +71 -0
  150. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  151. package/dist/sandbox/pods/reconcile.js +152 -0
  152. package/dist/sandbox/pods/spec.d.ts +45 -0
  153. package/dist/sandbox/pods/spec.js +62 -0
  154. package/dist/sandbox/records.d.ts +29 -0
  155. package/dist/sandbox/records.js +79 -0
  156. package/dist/sandbox/resolve.d.ts +5 -0
  157. package/dist/sandbox/resolve.js +3 -2
  158. package/dist/sandbox/session-sandbox.js +7 -5
  159. package/dist/sandbox/share.d.ts +11 -0
  160. package/dist/sandbox/share.js +10 -0
  161. package/dist/sandbox/tenant-config.d.ts +16 -1
  162. package/dist/sandbox/tenant-config.js +9 -0
  163. package/dist/sandbox/types.d.ts +29 -1
  164. package/dist/sandbox/types.js +13 -1
  165. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  166. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  167. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  168. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  170. package/dist/store/postgres/record-writer.d.ts +6 -1
  171. package/dist/store/postgres/record-writer.js +20 -2
  172. package/dist/store/postgres/schema.d.ts +894 -0
  173. package/dist/store/postgres/schema.js +130 -2
  174. package/dist/store/postgres/store.js +289 -4
  175. package/dist/store/types.d.ts +143 -6
  176. package/dist/streams/control.d.ts +2 -0
  177. package/dist/streams/control.js +5 -0
  178. package/dist/streams/types.d.ts +10 -1
  179. package/dist/tenant/advance.js +218 -160
  180. package/dist/tenant/artifact-tool.d.ts +5 -0
  181. package/dist/tenant/artifact-tool.js +119 -0
  182. package/dist/tenant/commands.js +30 -5
  183. package/dist/tenant/context.d.ts +64 -5
  184. package/dist/tenant/effects.d.ts +19 -13
  185. package/dist/tenant/effects.js +5 -382
  186. package/dist/tenant/history.d.ts +13 -23
  187. package/dist/tenant/history.js +14 -62
  188. package/dist/tenant/host-token.d.ts +92 -0
  189. package/dist/tenant/host-token.js +179 -0
  190. package/dist/tenant/module.js +1 -0
  191. package/dist/tenant/paths.d.ts +2 -1
  192. package/dist/tenant/paths.js +3 -1
  193. package/dist/tenant/reset.d.ts +5 -2
  194. package/dist/tenant/reset.js +22 -2
  195. package/dist/tenant/run-grants.d.ts +49 -0
  196. package/dist/tenant/run-grants.js +62 -0
  197. package/dist/tenant/run-token.d.ts +70 -0
  198. package/dist/tenant/run-token.js +138 -0
  199. package/dist/tenant/runtime.d.ts +38 -0
  200. package/dist/tenant/runtime.js +120 -32
  201. package/dist/tenant/sandboxes.d.ts +119 -0
  202. package/dist/tenant/sandboxes.js +551 -0
  203. package/dist/tenant/session.d.ts +2 -13
  204. package/dist/tenant/session.js +4 -28
  205. package/dist/tenant/sessions.d.ts +5 -0
  206. package/dist/tenant/sessions.js +28 -1
  207. package/dist/tenant/status.d.ts +8 -3
  208. package/dist/tenant/status.js +5 -0
  209. package/dist/tenant/streams.d.ts +7 -0
  210. package/dist/tenant/streams.js +24 -1
  211. package/dist/tenant/sweep.js +37 -4
  212. package/dist/tenant/tokens.js +9 -2
  213. package/dist/tenant/types.d.ts +18 -1
  214. package/dist/tenant/worker.d.ts +11 -5
  215. package/dist/tenant/worker.js +12 -6
  216. package/dist/version.d.ts +1 -1
  217. package/dist/version.js +1 -1
  218. package/package.json +13 -10
  219. package/dist/adapters/media.d.ts +0 -24
  220. package/dist/adapters/media.js +0 -111
@@ -1,58 +1,20 @@
1
1
  import { embeddedAgent, flowDelegateManifest, hashManifest, isWorkflowManifestV2, } from "@nylorun/core/define";
2
- import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowEffect, isFlowToolEffect, isWorkflowManifest, linkedMessageKey, linkedTurnEnd, } from "../core/flow-host.js";
2
+ import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowToolEffect, isWorkflowManifest, linkedMessageKey, linkedTurnEnd, } from "../core/flow-host.js";
3
3
  import { mayDispatchMore } from "../core/limits.js";
4
- import { canonical } from "../store/canonical.js";
5
- import { isOwnershipLost } from "../store/ownership.js";
6
4
  import { scrub } from "../redact.js";
7
- import { findServer, serversOf } from "../mcp/pool.js";
8
- import { sandboxCapabilityOf } from "../sandbox/manager.js";
5
+ import { isRemoteMcpCall } from "../harness/calls.js";
9
6
  import { owningSandboxSessionId, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
10
7
  import { withSandboxCapability } from "../sandbox/session-sandbox.js";
11
8
  import { loadSession, ownedSession, sandboxLookup, sessionOf, } from "./context.js";
12
9
  import { fail } from "./http.js";
13
- import { actionTarget, linkedAgentOutput, manifestFor, mcpToolOf, pinnedTool, turnManifestOf, } from "./session.js";
10
+ import { linkedAgentOutput, turnManifestOf } from "./session.js";
14
11
  import { command } from "./commands.js";
15
12
  import { offerAction } from "./delivery.js";
16
- import { assistantMessage, contextCompacted, modelFailed, toolCompleted, toolIds, } from "./transcript.js";
17
- import { classifyThrown } from "../model/classify.js";
18
- import { abortKind } from "./worker.js";
19
- /**
20
- * Call the model for one effect. A provider failure comes back as a failure outcome
21
- * (Model Calls §6), whichever provider serves the call; only an abort throws, and the
22
- * advance decides what the abort means.
23
- */
24
- export async function invokeModel(ctx, request, signal, model) {
25
- try {
26
- if (model)
27
- return await model(request, signal);
28
- if (!ctx.useVaultModel)
29
- return await ctx.modelProvider(request, signal);
30
- return await ctx.modelGate.call({
31
- tenantId: ctx.config.tenantId,
32
- sessionId: request.sessionId,
33
- turnId: request.turnId,
34
- agentId: request.agentId,
35
- effectId: request.effectId,
36
- invocationId: String(request.context.invocationId),
37
- call: request.input,
38
- }, signal);
39
- }
40
- catch (error) {
41
- if (signal.aborted) {
42
- // A call that outlives this process stops only when told to: on a user cancel. After a
43
- // shutdown or a lost lease the next owner re-sends it and picks up its outcome (P1.2).
44
- if (!model && ctx.useVaultModel && abortKind(signal) === "cancel" && ctx.modelGate.cancel)
45
- await ctx.modelGate.cancel({ tenantId: ctx.config.tenantId, effectId: request.effectId });
46
- throw error;
47
- }
48
- return classifyThrown(error);
49
- }
50
- }
51
13
  /**
52
14
  * The outcome of a flow `agent` effect once the linked turn it started ended, or undefined
53
15
  * while it has not. The linked session may still show an earlier iteration's turn.
54
16
  */
55
- async function linkedOutcome(t, effect, agent) {
17
+ export async function linkedOutcome(t, effect, agent) {
56
18
  const end = await linkedTurnEnd(t, effect, agent);
57
19
  if (!end || !agent)
58
20
  return undefined;
@@ -79,13 +41,7 @@ export function recoversMcpCalls(ctx) {
79
41
  }
80
42
  /** True when `request` calls a tool of a remote (`streamable-http` or `sse`) MCP server. */
81
43
  export function isRemoteMcpEffect(s, request) {
82
- if (request.kind !== "tool")
83
- return false;
84
- const tool = mcpToolOf(s, request);
85
- if (!tool)
86
- return false;
87
- const declared = findServer(s.manifest, tool.agentId, tool.capabilityId, tool.serverName);
88
- return declared !== undefined && declared.server.type !== "stdio";
44
+ return isRemoteMcpCall({ rootManifest: s.manifest, mcpSnapshot: s.mcpSnapshot }, request);
89
45
  }
90
46
  /**
91
47
  * True when this Tenant's vault-backed model calls outlive the process that sent them (the
@@ -95,242 +51,6 @@ export function isRemoteMcpEffect(s, request) {
95
51
  export function recoversModelCalls(ctx) {
96
52
  return ctx.useVaultModel && ctx.modelGate.recovers === true;
97
53
  }
98
- /**
99
- * What an effect's journal row must match on replay. A delegation's context carries only the
100
- * parent's tool call id, for its events; rows journaled before it existed still match.
101
- */
102
- function requestIdentity(request) {
103
- if (request.kind !== "delegation")
104
- return request;
105
- const { context: _, ...identity } = request;
106
- return identity;
107
- }
108
- function delegationCallId(request) {
109
- const callId = request.context?.callId;
110
- return typeof callId === "string" ? { callId } : {};
111
- }
112
- export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
113
- const { store } = ctx;
114
- const journaled = await store.tx(async (t) => {
115
- const s = await ownedSession(t, lease, request.sessionId);
116
- if (s.status === "cancelled" || s.activeTurnId !== request.turnId)
117
- throw new Error("Turn cancelled");
118
- // An aborted advance starts no effect; the advance decides what the abort means.
119
- signal.throwIfAborted();
120
- const resolved = (resolution) => ({
121
- kind: "resolved",
122
- resolution,
123
- });
124
- const existing = await t.get("effects", request.effectId);
125
- if (existing) {
126
- if (canonical(requestIdentity(existing.request)) !== canonical(requestIdentity(request)))
127
- throw new Error("Effect identity request drift");
128
- if (existing.status === "completed")
129
- return resolved({ status: "completed", outcome: existing.outcome });
130
- // A model call its previous owner left running at the gate: re-send it, same key and
131
- // request, to join it or collect its outcome.
132
- if (existing.status === "invoking" &&
133
- request.kind === "model" &&
134
- recoversModelCalls(ctx))
135
- return { kind: "invoke", invoke: "model", journaled: existing.request };
136
- // A remote MCP call its previous owner left running at the gate: the same, by effect id.
137
- if (existing.status === "invoking" &&
138
- recoversMcpCalls(ctx) &&
139
- isRemoteMcpEffect(s, existing.request))
140
- return {
141
- kind: "invoke",
142
- invoke: "mcp",
143
- journaled: existing.request,
144
- remote: true,
145
- };
146
- if (request.kind === "agent" && existing.status === "pending") {
147
- const agentSessionId = existing.agentSessionId;
148
- if (agentSessionId) {
149
- const agent = await t.get("sessions", agentSessionId);
150
- const outcome = await linkedOutcome(t, existing, agent);
151
- if (outcome) {
152
- existing.status = "completed";
153
- existing.outcome = outcome;
154
- await t.put("effects", request.effectId, existing);
155
- return resolved({ status: "completed", outcome });
156
- }
157
- }
158
- }
159
- return resolved({
160
- status: existing.status === "uncertain" || existing.status === "invoking"
161
- ? "uncertain"
162
- : "pending",
163
- });
164
- }
165
- if (request.kind === "agent" ||
166
- request.kind === "fn" ||
167
- request.kind === "verify" ||
168
- isFlowToolEffect(request)) {
169
- // Handled outside the agent-manifest path below.
170
- return { kind: "flow" };
171
- }
172
- if (request.kind === "delegation") {
173
- // Lifecycle points of an agent used as a tool: journaled once, so replays never re-emit.
174
- const outcome = { value: null };
175
- await t.put("effects", request.effectId, {
176
- request,
177
- status: "completed",
178
- outcome,
179
- });
180
- const settled = request.effectId.endsWith(":settled");
181
- await t.event(s.id, s.activeTurnId, settled ? "delegation.completed" : "delegation.started", {
182
- agent: request.agent,
183
- ...delegationCallId(request),
184
- ...request.input,
185
- });
186
- return resolved({ status: "completed", outcome });
187
- }
188
- const agentManifest = manifestFor(s.manifest, request.agent);
189
- if (!agentManifest)
190
- throw new Error(`Agent '${request.agent?.id ?? ""}' is not used as a tool`);
191
- const mcpTool = request.kind === "tool" ? mcpToolOf(s, request) : undefined;
192
- const sandboxTool = request.kind === "tool" && !mcpTool
193
- ? sandboxCapabilityOf(agentManifest, request.capabilityId, request.toolName)
194
- : undefined;
195
- await t.put("effects", request.effectId, {
196
- request,
197
- status: request.kind === "model" || mcpTool || sandboxTool
198
- ? "invoking"
199
- : "pending",
200
- });
201
- if (request.kind === "model" || mcpTool || sandboxTool)
202
- return {
203
- kind: "invoke",
204
- invoke: mcpTool ? "mcp" : sandboxTool ? "sandbox" : "model",
205
- ...(mcpTool && isRemoteMcpEffect(s, request) ? { remote: true } : {}),
206
- };
207
- const tool = request.kind === "tool"
208
- ? pinnedTool(agentManifest, request.capabilityId, request.toolName)
209
- : undefined;
210
- const base = {
211
- actionId: request.effectId,
212
- sessionId: request.sessionId,
213
- turnId: request.turnId,
214
- agentId: request.agentId,
215
- manifestHash: request.manifestHash,
216
- implementationVersion: s.implementationVersion,
217
- input: request.input,
218
- context: request.context,
219
- status: "pending",
220
- generation: 0,
221
- ...(request.agent ? { agent: request.agent } : {}),
222
- };
223
- const action = request.kind === "hook"
224
- ? {
225
- ...base,
226
- kind: "hook",
227
- hook: {
228
- at: request.hook.at,
229
- scope: request.hook.scope,
230
- capabilityIds: [...request.hook.capabilityIds],
231
- },
232
- }
233
- : {
234
- ...base,
235
- kind: "tool",
236
- capabilityId: request.capabilityId,
237
- toolName: request.toolName,
238
- ...(tool?.inputSchema ? { inputSchema: tool.inputSchema } : {}),
239
- ...(tool?.outputSchema ? { outputSchema: tool.outputSchema } : {}),
240
- };
241
- await t.put("actions", action.actionId, action);
242
- await t.event(s.id, s.activeTurnId, "action.pending", {
243
- actionId: action.actionId,
244
- kind: action.kind,
245
- ...actionTarget(action),
246
- ...(action.kind === "tool" ? toolIds(request.context) : {}),
247
- input: action.input,
248
- });
249
- await offerAction(t, ctx, action);
250
- return resolved({ status: "pending" });
251
- });
252
- if (journaled.kind === "resolved")
253
- return journaled.resolution;
254
- if (journaled.kind === "flow") {
255
- if (isFlowEffect(request))
256
- return resolveNewFlowEffect(ctx, request, signal, lease);
257
- return { status: "pending" };
258
- }
259
- const invoke = journaled.invoke;
260
- try {
261
- // The intent is committed; the call itself runs outside any transaction.
262
- const value = invoke === "mcp"
263
- ? await callMcpTool(ctx, journaled.journaled ?? request, abortOn(signal, journaled.remote && recoversMcpCalls(ctx) ? ["cancel", "shutdown"] : ["cancel"]))
264
- : invoke === "sandbox"
265
- ? await callSandboxTool(ctx, request, signal)
266
- : await invokeModel(ctx, journaled.journaled ?? request, signal, segment.model);
267
- return await store.tx(async (t) => {
268
- const s = await ownedSession(t, lease, request.sessionId);
269
- // Only a cancel discards an outcome in hand (§10.7). After any other abort (shutdown,
270
- // deadline) it is recorded, so the next advance replays it instead of calling again.
271
- if (s.status === "cancelled" ||
272
- s.activeTurnId !== request.turnId ||
273
- abortKind(signal) === "cancel")
274
- throw new Error("Turn cancelled");
275
- const effect = await t.get("effects", request.effectId);
276
- effect.status = "completed";
277
- effect.outcome = { value };
278
- await t.put("effects", request.effectId, effect);
279
- const failed = invoke === "model" ? modelFailed(request, value) : undefined;
280
- // A summary call is never an assistant message; the last one publishes context.compacted.
281
- const summarizing = invoke === "model" && Boolean(request.context.compaction);
282
- const compacted = summarizing && !failed ? contextCompacted(request, value) : undefined;
283
- const transcript = failed || summarizing
284
- ? undefined
285
- : invoke === "model"
286
- ? assistantMessage(request, value)
287
- : toolCompleted(request, value);
288
- if (failed)
289
- await t.event(s.id, request.turnId, "model.failed", failed);
290
- if (compacted)
291
- await t.event(s.id, request.turnId, "context.compacted", compacted);
292
- if (transcript && invoke === "model")
293
- await t.event(s.id, request.turnId, "message.assistant", transcript);
294
- else if (transcript)
295
- await t.event(s.id, request.turnId, "tool.completed", transcript);
296
- return { status: "completed", outcome: effect.outcome };
297
- });
298
- }
299
- catch (error) {
300
- // A lost epoch writes nothing: the new owner decides what the effect became.
301
- if (isOwnershipLost(error))
302
- throw error;
303
- // A shutdown leaves a model call running at the gate, still `invoking`: the next advance
304
- // re-sends it (P1.2). The segment stops for the shutdown as usual.
305
- if (invoke === "model" && recoversModelCalls(ctx) && abortKind(signal) === "shutdown")
306
- throw error;
307
- // The same for a remote MCP call at the Tool Gate (G3). A user cancel stops it there: a
308
- // keyed call outlives the request that sent it.
309
- if (invoke === "mcp" && journaled.remote && recoversMcpCalls(ctx)) {
310
- if (abortKind(signal) === "shutdown")
311
- throw error;
312
- if (abortKind(signal) === "cancel" && ctx.toolGate.cancel)
313
- await ctx.toolGate.cancel({ tenantId: ctx.config.tenantId, effectId: request.effectId });
314
- }
315
- await store.tx(async (t) => {
316
- const s = request.sessionId === lease.sessionId
317
- ? await t.assertEpoch(request.sessionId, lease.epoch)
318
- : await t.lockSession(request.sessionId);
319
- const effect = await t.get("effects", request.effectId);
320
- if (!effect)
321
- return;
322
- effect.status = "uncertain";
323
- effect.error = error instanceof Error ? error.message : String(error);
324
- await t.put("effects", request.effectId, effect);
325
- if (s && s.status !== "cancelled" && s.activeTurnId === request.turnId)
326
- await t.event(s.id, request.turnId, "effect.uncertain", {
327
- effectId: request.effectId,
328
- message: effect.error,
329
- });
330
- });
331
- return { status: "uncertain" };
332
- }
333
- }
334
54
  /** Journal and dispatch a new flow effect (agent / tool node / fn / verify). */
335
55
  export async function resolveNewFlowEffect(ctx, request, signal, lease) {
336
56
  const { store } = ctx;
@@ -551,91 +271,6 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
551
271
  return { status: "pending" };
552
272
  });
553
273
  }
554
- /** Discover the session's MCP tools once, or reconnect and refresh diagnostics. */
555
- export async function prepareMcp(ctx, lease, signal) {
556
- const { store } = ctx;
557
- const id = lease.sessionId;
558
- const s = await loadSession(ctx, id);
559
- if (serversOf(s.manifest).length === 0)
560
- return;
561
- if (!s.mcpSnapshot) {
562
- const found = await ctx.mcp.discover({
563
- sessionId: id,
564
- manifest: s.manifest,
565
- manifestHash: s.manifestHash,
566
- pluginRoots: s.pluginRoots ?? {},
567
- signal,
568
- });
569
- await store.tx(async (t) => {
570
- const current = await ownedSession(t, lease, id);
571
- if (current.mcpSnapshot)
572
- return;
573
- current.mcpSnapshot = found.snapshot;
574
- current.mcpDiagnostics = found.diagnostics;
575
- await t.put("sessions", id, current);
576
- });
577
- return;
578
- }
579
- const diagnostics = await ctx.mcp.reconnect({
580
- sessionId: id,
581
- manifest: s.manifest,
582
- pluginRoots: s.pluginRoots ?? {},
583
- tools: s.mcpSnapshot.mcpTools,
584
- signal,
585
- });
586
- if (diagnostics.length === 0)
587
- return;
588
- await store.tx(async (t) => {
589
- const current = await ownedSession(t, lease, id);
590
- const prior = [...(current.mcpDiagnostics ?? [])];
591
- for (const item of diagnostics) {
592
- const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId &&
593
- existing.serverName === item.serverName);
594
- if (index >= 0)
595
- prior[index] = item;
596
- else
597
- prior.push(item);
598
- }
599
- current.mcpDiagnostics = prior;
600
- await t.put("sessions", id, current);
601
- });
602
- }
603
- /**
604
- * A signal that follows `signal` only for the abort kinds in `kinds`. An MCP call stops on a
605
- * cancel; at the Tool Gate it also stops waiting on a shutdown, since the gate keeps the call
606
- * for the next owner. Otherwise a shutdown or a deadline lets the call finish and records it,
607
- * so the next advance replays it.
608
- */
609
- function abortOn(signal, kinds) {
610
- const controller = new AbortController();
611
- const follow = () => {
612
- if (kinds.includes(abortKind(signal)))
613
- controller.abort(signal.reason);
614
- };
615
- if (signal.aborted)
616
- follow();
617
- else
618
- signal.addEventListener("abort", follow, { once: true });
619
- return controller.signal;
620
- }
621
- async function callMcpTool(ctx, request, signal) {
622
- const s = await loadSession(ctx, request.sessionId);
623
- const tool = mcpToolOf(s, request);
624
- if (!tool)
625
- throw new Error(`MCP tool '${request.toolName ?? ""}' is not in the session snapshot`);
626
- return ctx.mcp.call({
627
- sessionId: s.id,
628
- ...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
629
- capabilityId: tool.capabilityId,
630
- serverName: tool.serverName,
631
- serverToolName: tool.serverToolName,
632
- args: request.input,
633
- manifest: s.manifest,
634
- pluginRoots: s.pluginRoots ?? {},
635
- effectId: request.effectId,
636
- signal,
637
- });
638
- }
639
274
  /**
640
275
  * The sandbox a linked session inherits from its tree's owner when that sandbox was chosen at
641
276
  * open (pinned on the owner). A definition that declares its own sandbox keeps it, as before.
@@ -651,18 +286,6 @@ function inheritedSandbox(owner, definition) {
651
286
  throw new Error(sandboxed.message);
652
287
  return { spec, manifest: sandboxed.manifest, manifestHash: sandboxed.manifestHash };
653
288
  }
654
- async function callSandboxTool(ctx, request, signal) {
655
- const { s, ownerId } = await ctx.store.tx(async (t) => {
656
- const s = await sessionOf(t, request.sessionId);
657
- // Agents used as tools share the session's sandbox; the tree declares one sandbox spec.
658
- const lookup = await sandboxLookup(t, s.id);
659
- return { s, ownerId: owningSandboxSessionId(s, lookup) };
660
- });
661
- const capability = sandboxCapabilityOf(manifestFor(s.manifest, request.agent), request.capabilityId, request.toolName);
662
- if (!capability)
663
- throw new Error(`'${request.toolName ?? ""}' is not a sandbox tool`);
664
- return ctx.sandbox.run({ id: ownerId, activeTurnId: s.activeTurnId, manifest: s.manifest }, capability, request.toolName, request.input, signal);
665
- }
666
289
  /** Vault authorization for an MCP server request made on behalf of a session. */
667
290
  export async function authorize(ctx, sessionId, request) {
668
291
  const s = await loadSession(ctx, sessionId);
@@ -10,18 +10,13 @@
10
10
  * restore the transcript from before that turn's first edit, as cancel and failure restore
11
11
  * `turnStartState`.
12
12
  * - **Lean rows.** The session row stores the engine state with an empty transcript
13
- * (`leanState`). `runSegment` folds the record from `Session.history.from` and hands the
13
+ * (`leanState`). The advance (`harness-api/start.ts`) folds the record from `Session.history.from` and hands the
14
14
  * engine the full transcript (`withTranscript`).
15
15
  * - **Shadow mode** (on in the runtime's tests, `test/setup/transcript-shadow.ts`) keeps the
16
16
  * transcript on the row too and checks the fold against it at every segment start.
17
17
  */
18
18
  import type { LiveEvent } from "@nylorun/core/contracts";
19
- /** The payload of one `transcript.updated` event. */
20
- export interface TranscriptUpdate {
21
- keep: number;
22
- entries: unknown[];
23
- length: number;
24
- }
19
+ export { CHUNK_BYTES, TranscriptFoldError, applyUpdate, applyUpdates, transcriptOf, transcriptUpdates, withTranscript, type TranscriptUpdate, } from "@nylorun/core/harness-api";
25
20
  /** Where a session's fold starts. */
26
21
  export interface SessionHistory {
27
22
  /** The fold reads the record from this seq: a snapshot at or before the turn's start. */
@@ -29,36 +24,31 @@ export interface SessionHistory {
29
24
  /** The latest snapshot (`keep: 0`), which `from` moves to when the next turn starts. */
30
25
  snapshot?: number;
31
26
  }
32
- /** An edit's entries are split so each event stays well under S2's 1 MiB record limit. */
33
- export declare const CHUNK_BYTES: number;
34
27
  /** Shadow mode: keep the transcript on the session row and check the fold against it. */
35
28
  export declare function transcriptShadow(): boolean;
36
29
  /** Tests switch shadow mode off to exercise lean rows. Returns the previous setting. */
37
30
  export declare function setTranscriptShadow(on: boolean): boolean;
38
- export declare class TranscriptFoldError extends Error {
39
- constructor(message: string);
40
- }
41
31
  /** The fold and the transcript on the row differ (shadow mode only). */
42
32
  export declare class TranscriptParityError extends Error {
43
33
  constructor(sessionId: string, at: number, folded: number, stored: number);
44
34
  }
45
- /** The engine state's transcript, or none. */
46
- export declare function transcriptOf(state: unknown): unknown[];
47
- /**
48
- * The edits from `before` to `after`, split into chunks of at most `CHUNK_BYTES` of entries.
49
- * Empty when nothing changed.
50
- */
51
- export declare function transcriptUpdates(before: readonly unknown[], after: readonly unknown[], chunkBytes?: number): TranscriptUpdate[];
52
- /** Applies one edit. Throws `TranscriptFoldError` when it does not fit the transcript. */
53
- export declare function applyUpdate(transcript: readonly unknown[], update: TranscriptUpdate): unknown[];
54
35
  /**
55
36
  * Folds a session's events, from the start of its record or from a snapshot, into the
56
37
  * transcript the engine resumes from.
57
38
  */
58
39
  export declare function foldTranscript(events: Iterable<Pick<LiveEvent, "type" | "turnId" | "payload">>): unknown[];
40
+ /**
41
+ * `foldTranscript`, and the cursor of the fold: the `seq` of the last event that changed it
42
+ * (`transcript.updated`, `turn.cancelled`, `turn.failed`), or -1. A harness that holds the
43
+ * transcript at the same cursor holds this one.
44
+ */
45
+ export declare function foldTranscriptWithCursor(events: Iterable<Pick<LiveEvent, "type" | "turnId" | "payload"> & {
46
+ seq?: number;
47
+ }>): {
48
+ transcript: unknown[];
49
+ cursor: number;
50
+ };
59
51
  /** `state` with an empty transcript (what the session row stores outside shadow mode). */
60
52
  export declare function leanState<T>(state: T): T;
61
- /** `state` with `transcript` (what the engine runs on). */
62
- export declare function withTranscript<T>(state: T, transcript: readonly unknown[]): T;
63
53
  /** In shadow mode, checks the fold against the transcript the row kept. */
64
54
  export declare function checkParity(sessionId: string, folded: readonly unknown[], stored: readonly unknown[]): void;
@@ -1,5 +1,5 @@
1
- /** An edit's entries are split so each event stays well under S2's 1 MiB record limit. */
2
- export const CHUNK_BYTES = 256 * 1024;
1
+ import { applyUpdate, transcriptOf } from "@nylorun/core/harness-api";
2
+ export { CHUNK_BYTES, TranscriptFoldError, applyUpdate, applyUpdates, transcriptOf, transcriptUpdates, withTranscript, } from "@nylorun/core/harness-api";
3
3
  let shadow = false;
4
4
  /** Shadow mode: keep the transcript on the session row and check the fold against it. */
5
5
  export function transcriptShadow() {
@@ -11,12 +11,6 @@ export function setTranscriptShadow(on) {
11
11
  shadow = on;
12
12
  return previous;
13
13
  }
14
- export class TranscriptFoldError extends Error {
15
- constructor(message) {
16
- super(message);
17
- this.name = "TranscriptFoldError";
18
- }
19
- }
20
14
  /** The fold and the transcript on the row differ (shadow mode only). */
21
15
  export class TranscriptParityError extends Error {
22
16
  constructor(sessionId, at, folded, stored) {
@@ -26,59 +20,21 @@ export class TranscriptParityError extends Error {
26
20
  }
27
21
  }
28
22
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
29
- /** The engine state's transcript, or none. */
30
- export function transcriptOf(state) {
31
- const transcript = state?.transcript;
32
- return Array.isArray(transcript) ? transcript : [];
33
- }
34
- /**
35
- * The edits from `before` to `after`, split into chunks of at most `CHUNK_BYTES` of entries.
36
- * Empty when nothing changed.
37
- */
38
- export function transcriptUpdates(before, after, chunkBytes = CHUNK_BYTES) {
39
- let keep = 0;
40
- while (keep < before.length && keep < after.length && same(before[keep], after[keep]))
41
- keep += 1;
42
- if (keep === before.length && keep === after.length)
43
- return [];
44
- const added = after.slice(keep);
45
- if (added.length === 0)
46
- return [{ keep, entries: [], length: keep }];
47
- const updates = [];
48
- let chunk = [];
49
- let bytes = 0;
50
- let base = keep;
51
- const flush = () => {
52
- updates.push({ keep: base, entries: chunk, length: base + chunk.length });
53
- base += chunk.length;
54
- chunk = [];
55
- bytes = 0;
56
- };
57
- for (const entry of added) {
58
- const size = JSON.stringify(entry).length;
59
- if (chunk.length > 0 && bytes + size > chunkBytes)
60
- flush();
61
- chunk.push(entry);
62
- bytes += size;
63
- }
64
- flush();
65
- return updates;
66
- }
67
- /** Applies one edit. Throws `TranscriptFoldError` when it does not fit the transcript. */
68
- export function applyUpdate(transcript, update) {
69
- if (update.keep > transcript.length)
70
- throw new TranscriptFoldError(`transcript.updated keeps ${update.keep} entries of ${transcript.length}`);
71
- const next = [...transcript.slice(0, update.keep), ...update.entries];
72
- if (next.length !== update.length)
73
- throw new TranscriptFoldError(`transcript.updated should give ${update.length} entries, not ${next.length}`);
74
- return next;
75
- }
76
23
  /**
77
24
  * Folds a session's events, from the start of its record or from a snapshot, into the
78
25
  * transcript the engine resumes from.
79
26
  */
80
27
  export function foldTranscript(events) {
28
+ return foldTranscriptWithCursor(events).transcript;
29
+ }
30
+ /**
31
+ * `foldTranscript`, and the cursor of the fold: the `seq` of the last event that changed it
32
+ * (`transcript.updated`, `turn.cancelled`, `turn.failed`), or -1. A harness that holds the
33
+ * transcript at the same cursor holds this one.
34
+ */
35
+ export function foldTranscriptWithCursor(events) {
81
36
  let transcript = [];
37
+ let cursor = -1;
82
38
  // The transcript before the current turn's first edit: what a cancel or failure restores.
83
39
  let turn;
84
40
  for (const event of events) {
@@ -86,16 +42,18 @@ export function foldTranscript(events) {
86
42
  if (!turn || turn.id !== event.turnId)
87
43
  turn = { id: event.turnId, before: transcript };
88
44
  transcript = applyUpdate(transcript, event.payload);
45
+ cursor = event.seq ?? cursor;
89
46
  }
90
47
  else if (event.type === "turn.cancelled" || event.type === "turn.failed") {
91
48
  if (turn && turn.id === event.turnId)
92
49
  transcript = turn.before;
93
50
  turn = undefined;
51
+ cursor = event.seq ?? cursor;
94
52
  }
95
53
  else if (event.type === "turn.completed")
96
54
  turn = undefined;
97
55
  }
98
- return transcript;
56
+ return { transcript, cursor };
99
57
  }
100
58
  /** `state` with an empty transcript (what the session row stores outside shadow mode). */
101
59
  export function leanState(state) {
@@ -103,12 +61,6 @@ export function leanState(state) {
103
61
  return state;
104
62
  return { ...state, transcript: [] };
105
63
  }
106
- /** `state` with `transcript` (what the engine runs on). */
107
- export function withTranscript(state, transcript) {
108
- if (!state)
109
- return state;
110
- return { ...state, transcript: [...transcript] };
111
- }
112
64
  /** In shadow mode, checks the fold against the transcript the row kept. */
113
65
  export function checkParity(sessionId, folded, stored) {
114
66
  if (!shadow)