@nylorun/runtime 0.22.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.
- package/CHANGELOG.md +161 -0
- package/README.md +95 -30
- package/dist/adapters/execution/restate.d.ts +15 -3
- package/dist/adapters/execution/restate.js +78 -84
- package/dist/adapters/replication/pgoutput.d.ts +2 -0
- package/dist/adapters/replication/pgoutput.js +6 -3
- package/dist/adapters/sandbox/local.d.ts +9 -1
- package/dist/adapters/sandbox/local.js +16 -4
- package/dist/adapters/sandbox/virtual.js +4 -1
- package/dist/adapters/streams/s2.js +1 -13
- package/dist/api/a2a/endpoint.js +5 -1
- package/dist/api/a2a/routes.d.ts +2 -0
- package/dist/api/a2a/routes.js +2 -2
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +4 -2
- package/dist/api/ag-ui/history.js +4 -1
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +11 -10
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +4953 -1204
- package/dist/api/components.js +25 -16
- package/dist/api/gate/keys.js +7 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +39 -3
- package/dist/api/http/app.js +4 -4
- package/dist/api/http/define.d.ts +6 -0
- package/dist/api/http/define.js +13 -1
- package/dist/api/http/respond.js +1 -1
- package/dist/api/http/routes/artifacts.js +35 -8
- package/dist/api/http/routes/coverage.d.ts +9 -0
- package/dist/api/http/routes/coverage.js +34 -0
- package/dist/api/http/routes/environments.d.ts +11 -0
- package/dist/api/http/routes/environments.js +283 -0
- package/dist/api/http/routes/mcp.d.ts +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.js +1 -1
- package/dist/api/http/routes/reads.d.ts +45 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +11 -6
- package/dist/api/http/routes/tenant.js +32 -10
- package/dist/api/openapi.js +31 -26
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +563 -0
- package/dist/artifacts/export.d.ts +1 -1
- package/dist/artifacts/export.js +2 -3
- package/dist/artifacts/folders.d.ts +6 -3
- package/dist/artifacts/folders.js +17 -12
- package/dist/artifacts/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +1 -1
- package/dist/artifacts/workspace.js +1 -1
- package/dist/core/flow-host.d.ts +3 -22
- package/dist/core/flow-host.js +11 -37
- package/dist/core/provider.d.ts +3 -2
- package/dist/core/provider.js +5 -37
- package/dist/core/sandbox-routes.d.ts +2 -21
- package/dist/core/sandbox-routes.js +10 -43
- package/dist/execution/types.d.ts +11 -9
- package/dist/execution/types.js +5 -7
- package/dist/gates/contract.d.ts +3 -24
- package/dist/gates/contract.js +1 -36
- package/dist/gates/egress-credentials.d.ts +9 -0
- package/dist/gates/egress-credentials.js +11 -4
- package/dist/gates/egress.d.ts +28 -1
- package/dist/gates/egress.js +115 -10
- package/dist/gates/fixture-model.d.ts +28 -0
- package/dist/gates/fixture-model.js +92 -0
- package/dist/gates/handler.js +4 -4
- package/dist/gates/http-tool.d.ts +6 -2
- package/dist/gates/http-tool.js +3 -5
- package/dist/gates/in-process.d.ts +5 -0
- package/dist/gates/in-process.js +12 -1
- package/dist/gates/journal.d.ts +30 -0
- package/dist/gates/journal.js +52 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/meter.d.ts +10 -2
- package/dist/gates/meter.js +20 -5
- package/dist/gates/policy.d.ts +49 -0
- package/dist/gates/policy.js +134 -0
- package/dist/gates/tenant-vaults.d.ts +7 -7
- package/dist/gates/tenant-vaults.js +5 -5
- package/dist/gates/tool-calls.d.ts +10 -5
- package/dist/gates/tool-calls.js +28 -10
- package/dist/gates/tool-client.d.ts +8 -0
- package/dist/gates/tool-client.js +27 -11
- package/dist/gates/tool-contract.d.ts +10 -68
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +12 -5
- package/dist/harness/calls.js +21 -12
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +18 -5
- package/dist/harness/pod.d.ts +34 -0
- package/dist/harness/pod.js +41 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +13 -5
- package/dist/harness/setup.d.ts +27 -0
- package/dist/harness/setup.js +150 -0
- package/dist/harness/ws-client.js +3 -2
- package/dist/harness/ws-port.d.ts +1 -5
- package/dist/harness/ws-port.js +1 -5
- package/dist/harness-api/in-process.js +3 -3
- package/dist/harness-api/record.d.ts +15 -1
- package/dist/harness-api/record.js +174 -44
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +8 -0
- package/dist/harness-api/server.js +93 -13
- package/dist/harness-api/start.d.ts +10 -3
- package/dist/harness-api/start.js +40 -10
- package/dist/harness-api/tool-journal.d.ts +96 -0
- package/dist/harness-api/tool-journal.js +102 -0
- package/dist/harness-api/tool-status.d.ts +13 -0
- package/dist/harness-api/tool-status.js +26 -0
- package/dist/harness-api/workspace.d.ts +6 -3
- package/dist/harness-api/workspace.js +27 -11
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/execution.d.ts +8 -6
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +64 -21
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +70 -0
- package/dist/host/stack-config.js +67 -1
- package/dist/infra/execution.d.ts +18 -1
- package/dist/infra/execution.js +28 -4
- package/dist/infra/index.d.ts +1 -1
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +5 -5
- package/dist/keys/keys.js +5 -7
- package/dist/management-openapi.json +999 -2211
- package/dist/mcp/connect.d.ts +25 -20
- package/dist/mcp/connect.js +29 -30
- package/dist/mcp/pool.d.ts +2 -2
- package/dist/mcp/pool.js +1 -1
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -20
- package/dist/mcp/snapshot.d.ts +5 -11
- package/dist/mcp/snapshot.js +6 -7
- package/dist/model/pi-model.js +8 -0
- package/dist/openapi.json +6955 -2526
- package/dist/reads/types.d.ts +16 -3
- package/dist/record/append.d.ts +3 -1
- package/dist/record/append.js +1 -0
- package/dist/record/envelope.d.ts +5 -1
- package/dist/record/envelope.js +7 -3
- package/dist/record/environment.d.ts +25 -0
- package/dist/record/environment.js +44 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/retry.d.ts +21 -0
- package/dist/retry.js +34 -0
- package/dist/sandbox/disk-fs.d.ts +95 -0
- package/dist/sandbox/disk-fs.js +206 -0
- package/dist/sandbox/join.d.ts +17 -18
- package/dist/sandbox/join.js +46 -20
- package/dist/sandbox/manager.d.ts +16 -8
- package/dist/sandbox/manager.js +33 -17
- package/dist/sandbox/placement.d.ts +13 -12
- package/dist/sandbox/placement.js +29 -12
- package/dist/sandbox/pods/client.d.ts +6 -1
- package/dist/sandbox/pods/lifecycle.d.ts +28 -27
- package/dist/sandbox/pods/lifecycle.js +14 -26
- package/dist/sandbox/pods/reconcile.js +21 -15
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +16 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +35 -27
- package/dist/sandbox/resolve.js +66 -41
- package/dist/sandbox/session-sandbox.js +1 -1
- package/dist/sandbox/setup.d.ts +61 -0
- package/dist/sandbox/setup.js +123 -0
- package/dist/sandbox/share.d.ts +11 -21
- package/dist/sandbox/share.js +12 -41
- package/dist/sandbox/tenant-config.d.ts +12 -14
- package/dist/sandbox/tenant-config.js +12 -17
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/store/postgres/control.js +8 -1
- package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
- package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -0
- package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
- package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
- package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
- package/dist/store/postgres/reads.js +117 -17
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/record.d.ts +7 -0
- package/dist/store/postgres/record.js +23 -2
- package/dist/store/postgres/schema.d.ts +1213 -779
- package/dist/store/postgres/schema.js +136 -76
- package/dist/store/postgres/store.js +283 -117
- package/dist/store/types.d.ts +206 -88
- package/dist/streams/relay/core.js +1 -13
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +121 -49
- package/dist/tenant/artifact-tool.js +3 -4
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +94 -14
- package/dist/tenant/context.d.ts +67 -33
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/control.d.ts +8 -1
- package/dist/tenant/control.js +2 -0
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +37 -20
- package/dist/tenant/environments.d.ts +167 -0
- package/dist/tenant/environments.js +823 -0
- package/dist/tenant/harness-tokens.d.ts +21 -0
- package/dist/tenant/harness-tokens.js +61 -0
- package/dist/tenant/host-token.d.ts +1 -1
- package/dist/tenant/host-token.js +2 -4
- package/dist/tenant/identity-file.d.ts +4 -4
- package/dist/tenant/identity-file.js +17 -8
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/principals.js +1 -1
- package/dist/tenant/reset.js +3 -3
- package/dist/tenant/runtime.d.ts +6 -1
- package/dist/tenant/runtime.js +30 -4
- package/dist/tenant/scheduler.d.ts +7 -0
- package/dist/tenant/scheduler.js +38 -7
- package/dist/tenant/session-streams.d.ts +0 -1
- package/dist/tenant/session-streams.js +1 -13
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +13 -13
- package/dist/tenant/sessions.js +61 -138
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/streams.js +14 -29
- package/dist/tenant/sweep.d.ts +39 -12
- package/dist/tenant/sweep.js +168 -63
- package/dist/tenant/transcript.d.ts +210 -20
- package/dist/tenant/transcript.js +62 -11
- package/dist/tenant/turn-timing.d.ts +43 -0
- package/dist/tenant/turn-timing.js +39 -0
- package/dist/tenant/types.d.ts +2 -0
- package/dist/traces/ids.d.ts +42 -0
- package/dist/traces/ids.js +85 -0
- package/dist/traces/key.d.ts +6 -0
- package/dist/traces/key.js +37 -0
- package/dist/traces/map.d.ts +113 -0
- package/dist/traces/map.js +317 -0
- package/dist/traces/options.d.ts +7 -0
- package/dist/traces/options.js +1 -0
- package/dist/traces/otlp.d.ts +42 -0
- package/dist/traces/otlp.js +84 -0
- package/dist/traces/projector.d.ts +58 -0
- package/dist/traces/projector.js +200 -0
- package/dist/traces/read.d.ts +23 -0
- package/dist/traces/read.js +16 -0
- package/dist/vault/coverage.d.ts +26 -0
- package/dist/vault/coverage.js +64 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/service.d.ts +47 -48
- package/dist/vault/service.js +321 -382
- package/dist/vault/sources.d.ts +5 -16
- package/dist/vault/sources.js +6 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/package.json +14 -13
- package/dist/api/http/routes/sandboxes.d.ts +0 -9
- package/dist/api/http/routes/sandboxes.js +0 -153
- package/dist/api/http/routes/vaults.d.ts +0 -10
- package/dist/api/http/routes/vaults.js +0 -139
- package/dist/record/sandbox.d.ts +0 -25
- package/dist/record/sandbox.js +0 -43
- package/dist/tenant/sandboxes.d.ts +0 -122
- package/dist/tenant/sandboxes.js +0 -562
package/dist/tenant/advance.js
CHANGED
|
@@ -40,9 +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
|
-
import {
|
|
46
|
+
import { endAdvance } from "./scheduler.js";
|
|
47
|
+
import { harnessOf } from "./context.js";
|
|
48
|
+
import { HARNESS_ID } from "../sandbox/placement.js";
|
|
49
|
+
import { recoversModelCalls } from "./effects.js";
|
|
46
50
|
import { slimModelEffects } from "./slim.js";
|
|
47
51
|
import { applyUpdates, leanState, transcriptShadow, withTranscript, } from "./history.js";
|
|
48
52
|
import { command } from "./commands.js";
|
|
@@ -50,7 +54,8 @@ import { exportOutputs } from "../artifacts/export.js";
|
|
|
50
54
|
import { dropRunGrant, grantRun } from "./run-grants.js";
|
|
51
55
|
import { startHeartbeat } from "../harness-api/renew.js";
|
|
52
56
|
import { buildTurnStart, startSegment } from "../harness-api/start.js";
|
|
53
|
-
import {
|
|
57
|
+
import { touchEnvironment } from "./environments.js";
|
|
58
|
+
import { endTurn, pauseTurn } from "./turn-timing.js";
|
|
54
59
|
import { AdvanceAbort, AdvanceDeadlineError, abortKind, } from "./worker.js";
|
|
55
60
|
/**
|
|
56
61
|
* A segment stopped by a `shutdown` or `ownership.lost` abort before it settled. Never
|
|
@@ -169,10 +174,7 @@ export async function advance(ctx, id, signal) {
|
|
|
169
174
|
heartbeat.stop();
|
|
170
175
|
dropRunGrant(ctx, lease);
|
|
171
176
|
signal.removeEventListener("abort", forward);
|
|
172
|
-
|
|
173
|
-
ctx.work.running.delete(id);
|
|
174
|
-
ctx.work.runningTurns.delete(id);
|
|
175
|
-
}
|
|
177
|
+
endAdvance(ctx.work, id, controller);
|
|
176
178
|
// Best effort: a release that fails leaves a lease that simply expires.
|
|
177
179
|
if (release)
|
|
178
180
|
await ctx.store
|
|
@@ -181,28 +183,27 @@ export async function advance(ctx, id, signal) {
|
|
|
181
183
|
sessionId: id,
|
|
182
184
|
message: error instanceof Error ? error.message : String(error),
|
|
183
185
|
}));
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
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);
|
|
187
190
|
}
|
|
188
191
|
return result;
|
|
189
192
|
}
|
|
190
|
-
/** Why a pod
|
|
193
|
+
/** Why a pod Environment cannot run a segment, or undefined when it can. */
|
|
191
194
|
function podUnusable(ctx, segment) {
|
|
192
195
|
const pod = segment.pod;
|
|
193
196
|
if (!pod)
|
|
194
197
|
return undefined;
|
|
195
198
|
if (!ctx.pods)
|
|
196
|
-
return `
|
|
199
|
+
return `Environment ${pod.id} is a pod Environment, and this Runtime has no sandbox pods`;
|
|
197
200
|
const state = pod.pod;
|
|
198
201
|
if (state.desired === "deleted")
|
|
199
|
-
return `
|
|
202
|
+
return `Environment ${pod.id} was deleted`;
|
|
200
203
|
if (state.observed === "lost")
|
|
201
|
-
return `
|
|
202
|
-
if (state.observed === "expired")
|
|
203
|
-
return `Sandbox ${pod.id} expired`;
|
|
204
|
+
return `Environment ${pod.id} was lost: ${state.reason ?? "its volume is gone"}`;
|
|
204
205
|
if (state.observed === "failed")
|
|
205
|
-
return `
|
|
206
|
+
return `Environment ${pod.id} failed to start: ${state.reason ?? "its pod was not ready"}`;
|
|
206
207
|
return undefined;
|
|
207
208
|
}
|
|
208
209
|
/**
|
|
@@ -231,7 +232,6 @@ async function takeOwnership(ctx, id) {
|
|
|
231
232
|
if (taken.takeover)
|
|
232
233
|
await takeOver(t, s, {
|
|
233
234
|
recoversModelCalls: recoversModelCalls(ctx),
|
|
234
|
-
recoversToolCalls: recoversToolCalls(ctx),
|
|
235
235
|
});
|
|
236
236
|
if (!s.checkpoint || !["running", "runnable"].includes(s.status)) {
|
|
237
237
|
await t.releaseOwnership(id, lease.owner, lease.epoch);
|
|
@@ -245,10 +245,12 @@ async function takeOwnership(ctx, id) {
|
|
|
245
245
|
/**
|
|
246
246
|
* Takeover (§10.5 step 2, §11.4), inside the transaction that took ownership: the dead
|
|
247
247
|
* owner's `invoking` effects become `uncertain` and are never invoked again, except model
|
|
248
|
-
* calls when the gate recovers them (P1.2) and
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
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.
|
|
252
254
|
*/
|
|
253
255
|
export async function takeOver(t, s, options = {}) {
|
|
254
256
|
const marked = [];
|
|
@@ -256,7 +258,7 @@ export async function takeOver(t, s, options = {}) {
|
|
|
256
258
|
// Still running at the gate, or finished there: the replay re-sends it (P1.2).
|
|
257
259
|
if (options.recoversModelCalls && effect.request.kind === "model")
|
|
258
260
|
continue;
|
|
259
|
-
if (
|
|
261
|
+
if (effect.request.kind === "tool")
|
|
260
262
|
continue;
|
|
261
263
|
effect.status = "uncertain";
|
|
262
264
|
effect.error ??= "The Worker running this effect stopped before its outcome was recorded";
|
|
@@ -281,13 +283,16 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
|
|
|
281
283
|
let end;
|
|
282
284
|
try {
|
|
283
285
|
const segment = await startSegment(ctx, lease, { harness: true });
|
|
284
|
-
// A pod
|
|
286
|
+
// A pod Environment's segment runs in its pod (F7.2), or fails when the pod cannot run it.
|
|
285
287
|
const unusable = podUnusable(ctx, segment);
|
|
286
288
|
if (unusable)
|
|
287
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();
|
|
288
293
|
const pod = segment.pod?.id;
|
|
289
|
-
if (
|
|
290
|
-
placed.
|
|
294
|
+
if (started.environmentId !== undefined)
|
|
295
|
+
placed.environment = started.environmentId;
|
|
291
296
|
end = await ctx.harness.offer({
|
|
292
297
|
lease,
|
|
293
298
|
start: buildTurnStart(ctx, segment),
|
|
@@ -295,6 +300,8 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
|
|
|
295
300
|
transcript: segment.transcript,
|
|
296
301
|
run,
|
|
297
302
|
onTaken,
|
|
303
|
+
// Only a connection of the session's harness takes its runs (R4.1).
|
|
304
|
+
harness: harnessOf(segment.current),
|
|
298
305
|
...(pod === undefined ? {} : { pod, waitMs: POD_OFFER_WAIT_MS }),
|
|
299
306
|
});
|
|
300
307
|
if (end.kind === "unavailable") {
|
|
@@ -312,7 +319,8 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
|
|
|
312
319
|
stopIfLeaving(signal);
|
|
313
320
|
if (end.kind === "released")
|
|
314
321
|
throw new SegmentStopped(end.reason === "ownership.lost" ? "ownership.lost" : "shutdown");
|
|
315
|
-
|
|
322
|
+
// A harness's output never carries the Runtime's own tokens into the record (R4).
|
|
323
|
+
const output = redactTokens(end.output);
|
|
316
324
|
if (output.thrown)
|
|
317
325
|
throw thrownError(output.thrown);
|
|
318
326
|
const result = resultOf(output, segment);
|
|
@@ -322,7 +330,9 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
|
|
|
322
330
|
await settleFailure(ctx, lease, started, signal.reason);
|
|
323
331
|
else {
|
|
324
332
|
const recorded = result.status === "yielded" || result.status === "completed" || result.status === "paused";
|
|
325
|
-
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 });
|
|
326
336
|
}
|
|
327
337
|
end.reply(cursor === undefined ? {} : { cursor });
|
|
328
338
|
}
|
|
@@ -363,7 +373,8 @@ function resultOf(output, segment) {
|
|
|
363
373
|
effectIds: output.effectIds ?? [],
|
|
364
374
|
};
|
|
365
375
|
let state = output.state;
|
|
366
|
-
|
|
376
|
+
// Our engine's transcript shadow (tests); another harness's state is stored as sent (R4.2).
|
|
377
|
+
if (state !== undefined && transcriptShadow() && ours(current))
|
|
367
378
|
state = withTranscript(state, applyUpdates(segment.transcript, output.transcript ?? []));
|
|
368
379
|
const checkpoint = state === undefined || isWorkflowManifest(current.manifest)
|
|
369
380
|
? current.checkpoint
|
|
@@ -395,7 +406,6 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
|
|
|
395
406
|
error: type === "turn.failed"
|
|
396
407
|
? String(payload.message ?? "")
|
|
397
408
|
: undefined,
|
|
398
|
-
schedule: ctx.wake,
|
|
399
409
|
});
|
|
400
410
|
else if (type === "turn.cancelled")
|
|
401
411
|
await wakeLinkedWorkflow({
|
|
@@ -404,7 +414,6 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
|
|
|
404
414
|
turnId,
|
|
405
415
|
cancelled: true,
|
|
406
416
|
error: "Agent turn was cancelled",
|
|
407
|
-
schedule: ctx.wake,
|
|
408
417
|
});
|
|
409
418
|
}
|
|
410
419
|
/**
|
|
@@ -412,8 +421,8 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
|
|
|
412
421
|
* the turn has committed and while the advance still holds the lease. The harness hears back
|
|
413
422
|
* after the export. The export never throws.
|
|
414
423
|
*/
|
|
415
|
-
async function settleTurn(ctx, lease, s, result, updates, signal, cursor) {
|
|
416
|
-
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);
|
|
417
426
|
if (result.status === "completed")
|
|
418
427
|
await exportOutputs(ctx, lease.sessionId, s.activeTurnId, signal);
|
|
419
428
|
return kept;
|
|
@@ -423,7 +432,9 @@ async function settleTurn(ctx, lease, s, result, updates, signal, cursor) {
|
|
|
423
432
|
* cursor of the transcript it recorded (`cursor` when it recorded no edit), or undefined when
|
|
424
433
|
* the transcript was not kept.
|
|
425
434
|
*/
|
|
426
|
-
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) {
|
|
427
438
|
const id = lease.sessionId;
|
|
428
439
|
let kept;
|
|
429
440
|
const siblingCancelIds = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
@@ -434,11 +445,24 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
434
445
|
// A result can arrive during concurrent action persistence. Preserve the runnable marker.
|
|
435
446
|
const resumeRequested = current.status === "runnable";
|
|
436
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
|
+
}
|
|
437
460
|
const linkedWaits = isWorkflowManifest(current.manifest)
|
|
438
461
|
? await aggregateWaits({ t, workflowSessionId: id })
|
|
439
462
|
: [];
|
|
440
463
|
if (linkedWaits.length > 0 && !resumeRequested) {
|
|
441
464
|
current.status = "paused";
|
|
465
|
+
pauseTurn(current);
|
|
442
466
|
current.waits = linkedWaits;
|
|
443
467
|
}
|
|
444
468
|
else {
|
|
@@ -460,8 +484,8 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
460
484
|
if (result.status === "yielded") {
|
|
461
485
|
// The turn goes on in a new segment: same turn, next checkpoint, woken right away.
|
|
462
486
|
const finished = result.checkpoint;
|
|
463
|
-
kept = (await recordTranscript(t, id, s.activeTurnId, current, updates)) ?? cursor;
|
|
464
|
-
current.state =
|
|
487
|
+
kept = (await recordTranscript(t, id, s.activeTurnId, current, updates, writer)) ?? cursor;
|
|
488
|
+
current.state = leanOf(current, result.result.state);
|
|
465
489
|
current.checkpoint = {
|
|
466
490
|
...finished,
|
|
467
491
|
segment: finished.segment + 1,
|
|
@@ -473,22 +497,25 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
473
497
|
await slimModelEffects(t, id, s.activeTurnId);
|
|
474
498
|
await t.put("sessions", id, current);
|
|
475
499
|
const segment = finished.segment + 1;
|
|
476
|
-
await t.wake(id, {
|
|
500
|
+
await t.wake(id, {
|
|
501
|
+
reason: "rollover",
|
|
502
|
+
dedupeKey: `rollover:${s.activeTurnId}:${segment}`,
|
|
503
|
+
});
|
|
477
504
|
return siblings;
|
|
478
505
|
}
|
|
479
506
|
const flow = isWorkflowManifest(current.manifest);
|
|
480
507
|
if (!flow) {
|
|
481
508
|
if (result.status !== "failed") {
|
|
482
|
-
const last = await recordTranscript(t, id, s.activeTurnId, current, updates);
|
|
509
|
+
const last = await recordTranscript(t, id, s.activeTurnId, current, updates, writer);
|
|
483
510
|
if (result.status !== "cancelled")
|
|
484
511
|
kept = last ?? cursor;
|
|
485
512
|
}
|
|
486
513
|
current.state =
|
|
487
514
|
result.status === "failed"
|
|
488
515
|
? current.turnStartState
|
|
489
|
-
:
|
|
516
|
+
: leanOf(current, result.result.state);
|
|
490
517
|
}
|
|
491
|
-
current.checkpoint = leanCheckpoint(result.checkpoint);
|
|
518
|
+
current.checkpoint = leanCheckpoint(current, result.checkpoint);
|
|
492
519
|
current.status = result.status;
|
|
493
520
|
current.waits =
|
|
494
521
|
result.status === "paused" ? result.result.pending : undefined;
|
|
@@ -509,7 +536,11 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
509
536
|
});
|
|
510
537
|
siblings.push(...cancelResult.agentSessionIds);
|
|
511
538
|
}
|
|
512
|
-
|
|
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 {
|
|
513
544
|
current.activeTurnId = null;
|
|
514
545
|
if (s.activeTurnId)
|
|
515
546
|
current.lastTurnId = s.activeTurnId;
|
|
@@ -517,7 +548,7 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
517
548
|
}
|
|
518
549
|
const type = `turn.${result.status}`;
|
|
519
550
|
const payload = result.status === "completed"
|
|
520
|
-
? { output: result.result.output }
|
|
551
|
+
? { output: result.result.output, ...ended }
|
|
521
552
|
: result.status === "paused"
|
|
522
553
|
? {
|
|
523
554
|
interactions: (result.result.pending ?? []).map((call) => ({
|
|
@@ -540,9 +571,10 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
540
571
|
? { path: result.result.error.path }
|
|
541
572
|
: {}),
|
|
542
573
|
},
|
|
574
|
+
...ended,
|
|
543
575
|
}
|
|
544
|
-
: {};
|
|
545
|
-
await t.event(id, s.activeTurnId, type, payload);
|
|
576
|
+
: { ...ended };
|
|
577
|
+
await t.event(id, s.activeTurnId, type, payload, writer);
|
|
546
578
|
await t.put("sessions", id, current);
|
|
547
579
|
await wakeWorkflowOf(ctx, t, current, s.activeTurnId, type, payload);
|
|
548
580
|
return siblings;
|
|
@@ -565,6 +597,37 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
565
597
|
}
|
|
566
598
|
return kept;
|
|
567
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
|
+
}
|
|
568
631
|
/** A segment threw: fail the turn, restore the turn-start state, wake a linked workflow. */
|
|
569
632
|
async function settleFailure(ctx, lease, s, error) {
|
|
570
633
|
const id = lease.sessionId;
|
|
@@ -575,7 +638,7 @@ async function settleFailure(ctx, lease, s, error) {
|
|
|
575
638
|
current.status = "failed";
|
|
576
639
|
current.state = current.turnStartState;
|
|
577
640
|
current.error = error instanceof Error ? error.message : String(error);
|
|
578
|
-
const payload = { message: current.error };
|
|
641
|
+
const payload = { message: current.error, ...(await endTurn(t, current, current.activeTurnId)) };
|
|
579
642
|
await t.event(id, current.activeTurnId, "turn.failed", payload);
|
|
580
643
|
await slimModelEffects(t, id, current.activeTurnId);
|
|
581
644
|
if (current.activeTurnId)
|
|
@@ -585,21 +648,30 @@ async function settleFailure(ctx, lease, s, error) {
|
|
|
585
648
|
await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.failed", payload);
|
|
586
649
|
});
|
|
587
650
|
}
|
|
588
|
-
/**
|
|
589
|
-
|
|
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) {
|
|
590
662
|
const state = checkpoint?.state;
|
|
591
663
|
if (!checkpoint || !state)
|
|
592
664
|
return checkpoint;
|
|
593
|
-
return { ...checkpoint, state:
|
|
665
|
+
return { ...checkpoint, state: leanOf(session, state) };
|
|
594
666
|
}
|
|
595
667
|
/**
|
|
596
668
|
* Appends a segment's transcript edits; a snapshot moves `history.snapshot`. Returns the seq of
|
|
597
669
|
* the last edit, if any.
|
|
598
670
|
*/
|
|
599
|
-
async function recordTranscript(t, id, turnId, s, updates) {
|
|
671
|
+
async function recordTranscript(t, id, turnId, s, updates, writer) {
|
|
600
672
|
let last;
|
|
601
673
|
for (const update of updates) {
|
|
602
|
-
const event = await t.event(id, turnId, "transcript.updated", update);
|
|
674
|
+
const event = await t.event(id, turnId, "transcript.updated", update, writer);
|
|
603
675
|
if (update.keep === 0)
|
|
604
676
|
s.history = { from: s.history?.from ?? event.seq, snapshot: event.seq };
|
|
605
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 {
|
|
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
|
|
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.
|
|
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
|
-
|
|
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;
|
package/dist/tenant/commands.js
CHANGED
|
@@ -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 {
|
|
27
|
-
import {
|
|
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
|
-
|
|
83
|
-
|
|
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
|
|
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,18 +265,21 @@ 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
|
|
227
|
-
|
|
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 = {
|
|
231
278
|
reason: command.type,
|
|
232
279
|
dedupeKey: `${command.type}:${s.activeTurnId}:${cause}`,
|
|
233
280
|
};
|
|
234
|
-
await t.wake(id, commandWake
|
|
235
|
-
event = await t.event(id, s.activeTurnId, `command.${command.type}`, command);
|
|
281
|
+
await t.wake(id, commandWake);
|
|
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
|
|
@@ -244,7 +291,6 @@ export async function command(ctx, id, input, scope) {
|
|
|
244
291
|
turnId: cancelledTurnId,
|
|
245
292
|
cancelled: true,
|
|
246
293
|
error: "Agent turn was cancelled",
|
|
247
|
-
schedule: ctx.wake,
|
|
248
294
|
});
|
|
249
295
|
const response = {
|
|
250
296
|
status: "accepted",
|
|
@@ -273,3 +319,37 @@ export async function command(ctx, id, input, scope) {
|
|
|
273
319
|
}
|
|
274
320
|
return response;
|
|
275
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
|
+
}
|