@nylorun/runtime 0.23.0-beta → 0.24.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -0
- package/README.md +79 -28
- package/dist/adapters/execution/restate.js +1 -1
- 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/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 +2 -3
- package/dist/core/flow-host.js +3 -0
- 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 +1 -1
- 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/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +54 -5
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +64 -0
- package/dist/host/stack-config.js +61 -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/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 +42 -15
- 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 +18 -12
- 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/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 +28 -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 +934 -761
- package/dist/store/postgres/schema.js +102 -73
- package/dist/store/postgres/store.js +119 -66
- package/dist/store/types.d.ts +109 -48
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +115 -42
- package/dist/tenant/artifact-tool.js +3 -4
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +93 -12
- package/dist/tenant/context.d.ts +48 -22
- package/dist/tenant/context.js +4 -15
- 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 +7 -1
- 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/sweep.js +12 -6
- 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 +13 -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 -123
- package/dist/tenant/sandboxes.js +0 -566
package/dist/tenant/advance.js
CHANGED
|
@@ -40,10 +40,13 @@
|
|
|
40
40
|
* already recorded; an `ownership.lost` leaves the session to the advance that owns it.
|
|
41
41
|
*/
|
|
42
42
|
import { runDurable, runFlowDurable, } from "@nylorun/harness/run";
|
|
43
|
+
import { redactTokens } from "../harness-api/redact.js";
|
|
43
44
|
import { aggregateWaits, cancelSiblingWork, isWorkflowManifest, wakeLinkedWorkflow, } from "../core/flow-host.js";
|
|
44
45
|
import { isOwnershipLost, ownedTx } from "../store/ownership.js";
|
|
45
46
|
import { endAdvance } from "./scheduler.js";
|
|
46
|
-
import {
|
|
47
|
+
import { harnessOf } from "./context.js";
|
|
48
|
+
import { HARNESS_ID } from "../sandbox/placement.js";
|
|
49
|
+
import { recoversModelCalls } from "./effects.js";
|
|
47
50
|
import { slimModelEffects } from "./slim.js";
|
|
48
51
|
import { applyUpdates, leanState, transcriptShadow, withTranscript, } from "./history.js";
|
|
49
52
|
import { command } from "./commands.js";
|
|
@@ -51,7 +54,8 @@ import { exportOutputs } from "../artifacts/export.js";
|
|
|
51
54
|
import { dropRunGrant, grantRun } from "./run-grants.js";
|
|
52
55
|
import { startHeartbeat } from "../harness-api/renew.js";
|
|
53
56
|
import { buildTurnStart, startSegment } from "../harness-api/start.js";
|
|
54
|
-
import {
|
|
57
|
+
import { touchEnvironment } from "./environments.js";
|
|
58
|
+
import { endTurn, pauseTurn } from "./turn-timing.js";
|
|
55
59
|
import { AdvanceAbort, AdvanceDeadlineError, abortKind, } from "./worker.js";
|
|
56
60
|
/**
|
|
57
61
|
* A segment stopped by a `shutdown` or `ownership.lost` abort before it settled. Never
|
|
@@ -179,28 +183,27 @@ export async function advance(ctx, id, signal) {
|
|
|
179
183
|
sessionId: id,
|
|
180
184
|
message: error instanceof Error ? error.message : String(error),
|
|
181
185
|
}));
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
// The Environment was used: `deleteAfter`, and a pod's idle time (D34), count from the end
|
|
187
|
+
// of each segment.
|
|
188
|
+
if (placed.environment !== undefined)
|
|
189
|
+
await touchEnvironment(ctx, placed.environment);
|
|
185
190
|
}
|
|
186
191
|
return result;
|
|
187
192
|
}
|
|
188
|
-
/** Why a pod
|
|
193
|
+
/** Why a pod Environment cannot run a segment, or undefined when it can. */
|
|
189
194
|
function podUnusable(ctx, segment) {
|
|
190
195
|
const pod = segment.pod;
|
|
191
196
|
if (!pod)
|
|
192
197
|
return undefined;
|
|
193
198
|
if (!ctx.pods)
|
|
194
|
-
return `
|
|
199
|
+
return `Environment ${pod.id} is a pod Environment, and this Runtime has no sandbox pods`;
|
|
195
200
|
const state = pod.pod;
|
|
196
201
|
if (state.desired === "deleted")
|
|
197
|
-
return `
|
|
202
|
+
return `Environment ${pod.id} was deleted`;
|
|
198
203
|
if (state.observed === "lost")
|
|
199
|
-
return `
|
|
200
|
-
if (state.observed === "expired")
|
|
201
|
-
return `Sandbox ${pod.id} expired`;
|
|
204
|
+
return `Environment ${pod.id} was lost: ${state.reason ?? "its volume is gone"}`;
|
|
202
205
|
if (state.observed === "failed")
|
|
203
|
-
return `
|
|
206
|
+
return `Environment ${pod.id} failed to start: ${state.reason ?? "its pod was not ready"}`;
|
|
204
207
|
return undefined;
|
|
205
208
|
}
|
|
206
209
|
/**
|
|
@@ -229,7 +232,6 @@ async function takeOwnership(ctx, id) {
|
|
|
229
232
|
if (taken.takeover)
|
|
230
233
|
await takeOver(t, s, {
|
|
231
234
|
recoversModelCalls: recoversModelCalls(ctx),
|
|
232
|
-
recoversToolCalls: recoversToolCalls(ctx),
|
|
233
235
|
});
|
|
234
236
|
if (!s.checkpoint || !["running", "runnable"].includes(s.status)) {
|
|
235
237
|
await t.releaseOwnership(id, lease.owner, lease.epoch);
|
|
@@ -243,10 +245,12 @@ async function takeOwnership(ctx, id) {
|
|
|
243
245
|
/**
|
|
244
246
|
* Takeover (§10.5 step 2, §11.4), inside the transaction that took ownership: the dead
|
|
245
247
|
* owner's `invoking` effects become `uncertain` and are never invoked again, except model
|
|
246
|
-
* calls when the gate recovers them (P1.2) and
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
248
|
+
* calls when the gate recovers them (P1.2) and every tool call (R5.3): those stay `invoking`,
|
|
249
|
+
* and the replay decides. It re-sends a model call, and an MCP or HTTP tool call with the same
|
|
250
|
+
* key; a sandbox or core-served tool call fails to the model as `tool.interrupted`
|
|
251
|
+
* (`harness-api/tool-journal.ts`). When an effect marked `uncertain` belongs to the active
|
|
252
|
+
* turn, the session becomes `uncertain` too, with an `effect.uncertain` event. Mutates and
|
|
253
|
+
* writes `s`. Returns the effect ids it marked.
|
|
250
254
|
*/
|
|
251
255
|
export async function takeOver(t, s, options = {}) {
|
|
252
256
|
const marked = [];
|
|
@@ -254,7 +258,7 @@ export async function takeOver(t, s, options = {}) {
|
|
|
254
258
|
// Still running at the gate, or finished there: the replay re-sends it (P1.2).
|
|
255
259
|
if (options.recoversModelCalls && effect.request.kind === "model")
|
|
256
260
|
continue;
|
|
257
|
-
if (
|
|
261
|
+
if (effect.request.kind === "tool")
|
|
258
262
|
continue;
|
|
259
263
|
effect.status = "uncertain";
|
|
260
264
|
effect.error ??= "The Worker running this effect stopped before its outcome was recorded";
|
|
@@ -279,13 +283,16 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
|
|
|
279
283
|
let end;
|
|
280
284
|
try {
|
|
281
285
|
const segment = await startSegment(ctx, lease, { harness: true });
|
|
282
|
-
// A pod
|
|
286
|
+
// A pod Environment's segment runs in its pod (F7.2), or fails when the pod cannot run it.
|
|
283
287
|
const unusable = podUnusable(ctx, segment);
|
|
284
288
|
if (unusable)
|
|
285
289
|
throw new Error(unusable);
|
|
290
|
+
// A new volume being seeded from a disk version takes no turn until it holds it (R2d step 3).
|
|
291
|
+
if (segment.pod?.seeding)
|
|
292
|
+
throw new HarnessUnavailable();
|
|
286
293
|
const pod = segment.pod?.id;
|
|
287
|
-
if (
|
|
288
|
-
placed.
|
|
294
|
+
if (started.environmentId !== undefined)
|
|
295
|
+
placed.environment = started.environmentId;
|
|
289
296
|
end = await ctx.harness.offer({
|
|
290
297
|
lease,
|
|
291
298
|
start: buildTurnStart(ctx, segment),
|
|
@@ -293,6 +300,8 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
|
|
|
293
300
|
transcript: segment.transcript,
|
|
294
301
|
run,
|
|
295
302
|
onTaken,
|
|
303
|
+
// Only a connection of the session's harness takes its runs (R4.1).
|
|
304
|
+
harness: harnessOf(segment.current),
|
|
296
305
|
...(pod === undefined ? {} : { pod, waitMs: POD_OFFER_WAIT_MS }),
|
|
297
306
|
});
|
|
298
307
|
if (end.kind === "unavailable") {
|
|
@@ -310,7 +319,8 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
|
|
|
310
319
|
stopIfLeaving(signal);
|
|
311
320
|
if (end.kind === "released")
|
|
312
321
|
throw new SegmentStopped(end.reason === "ownership.lost" ? "ownership.lost" : "shutdown");
|
|
313
|
-
|
|
322
|
+
// A harness's output never carries the Runtime's own tokens into the record (R4).
|
|
323
|
+
const output = redactTokens(end.output);
|
|
314
324
|
if (output.thrown)
|
|
315
325
|
throw thrownError(output.thrown);
|
|
316
326
|
const result = resultOf(output, segment);
|
|
@@ -320,7 +330,9 @@ async function runRemoteSegment(ctx, lease, started, controller, run, onTaken, p
|
|
|
320
330
|
await settleFailure(ctx, lease, started, signal.reason);
|
|
321
331
|
else {
|
|
322
332
|
const recorded = result.status === "yielded" || result.status === "completed" || result.status === "paused";
|
|
323
|
-
cursor = await settleTurn(ctx, lease, started, result, recorded ? output.transcript ?? [] : [], signal, segment.cursor
|
|
333
|
+
cursor = await settleTurn(ctx, lease, started, result, recorded ? output.transcript ?? [] : [], signal, segment.cursor,
|
|
334
|
+
// The turn's end and its transcript are what the harness reported (R4.9).
|
|
335
|
+
{ source: { kind: "harness", id: harnessOf(started) }, evidence: "claimed", runId: output.runId });
|
|
324
336
|
}
|
|
325
337
|
end.reply(cursor === undefined ? {} : { cursor });
|
|
326
338
|
}
|
|
@@ -361,7 +373,8 @@ function resultOf(output, segment) {
|
|
|
361
373
|
effectIds: output.effectIds ?? [],
|
|
362
374
|
};
|
|
363
375
|
let state = output.state;
|
|
364
|
-
|
|
376
|
+
// Our engine's transcript shadow (tests); another harness's state is stored as sent (R4.2).
|
|
377
|
+
if (state !== undefined && transcriptShadow() && ours(current))
|
|
365
378
|
state = withTranscript(state, applyUpdates(segment.transcript, output.transcript ?? []));
|
|
366
379
|
const checkpoint = state === undefined || isWorkflowManifest(current.manifest)
|
|
367
380
|
? current.checkpoint
|
|
@@ -408,8 +421,8 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
|
|
|
408
421
|
* the turn has committed and while the advance still holds the lease. The harness hears back
|
|
409
422
|
* after the export. The export never throws.
|
|
410
423
|
*/
|
|
411
|
-
async function settleTurn(ctx, lease, s, result, updates, signal, cursor) {
|
|
412
|
-
const kept = await settle(ctx, lease, s, result, updates, cursor);
|
|
424
|
+
async function settleTurn(ctx, lease, s, result, updates, signal, cursor, writer) {
|
|
425
|
+
const kept = await settle(ctx, lease, s, result, updates, cursor, writer);
|
|
413
426
|
if (result.status === "completed")
|
|
414
427
|
await exportOutputs(ctx, lease.sessionId, s.activeTurnId, signal);
|
|
415
428
|
return kept;
|
|
@@ -419,7 +432,9 @@ async function settleTurn(ctx, lease, s, result, updates, signal, cursor) {
|
|
|
419
432
|
* cursor of the transcript it recorded (`cursor` when it recorded no edit), or undefined when
|
|
420
433
|
* the transcript was not kept.
|
|
421
434
|
*/
|
|
422
|
-
async function settle(ctx, lease, s, result, updates = [], cursor
|
|
435
|
+
async function settle(ctx, lease, s, result, updates = [], cursor,
|
|
436
|
+
/** Who reported the result (R4.9): the run's harness. */
|
|
437
|
+
writer) {
|
|
423
438
|
const id = lease.sessionId;
|
|
424
439
|
let kept;
|
|
425
440
|
const siblingCancelIds = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
@@ -430,11 +445,24 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
430
445
|
// A result can arrive during concurrent action persistence. Preserve the runnable marker.
|
|
431
446
|
const resumeRequested = current.status === "runnable";
|
|
432
447
|
if (result.status === "waiting" || result.status === "uncertain") {
|
|
448
|
+
// Tool calls the Tool Gate held for approval (R5 step 2): the turn pauses for a person.
|
|
449
|
+
const held = resumeRequested ? [] : await heldCalls(t, result.effectIds);
|
|
450
|
+
if (held.length > 0) {
|
|
451
|
+
current.status = "paused";
|
|
452
|
+
pauseTurn(current);
|
|
453
|
+
current.waits = held;
|
|
454
|
+
const payload = { interactions: held };
|
|
455
|
+
await t.event(id, s.activeTurnId, "turn.paused", payload);
|
|
456
|
+
await t.put("sessions", id, current);
|
|
457
|
+
await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.paused", payload);
|
|
458
|
+
return siblings;
|
|
459
|
+
}
|
|
433
460
|
const linkedWaits = isWorkflowManifest(current.manifest)
|
|
434
461
|
? await aggregateWaits({ t, workflowSessionId: id })
|
|
435
462
|
: [];
|
|
436
463
|
if (linkedWaits.length > 0 && !resumeRequested) {
|
|
437
464
|
current.status = "paused";
|
|
465
|
+
pauseTurn(current);
|
|
438
466
|
current.waits = linkedWaits;
|
|
439
467
|
}
|
|
440
468
|
else {
|
|
@@ -456,8 +484,8 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
456
484
|
if (result.status === "yielded") {
|
|
457
485
|
// The turn goes on in a new segment: same turn, next checkpoint, woken right away.
|
|
458
486
|
const finished = result.checkpoint;
|
|
459
|
-
kept = (await recordTranscript(t, id, s.activeTurnId, current, updates)) ?? cursor;
|
|
460
|
-
current.state =
|
|
487
|
+
kept = (await recordTranscript(t, id, s.activeTurnId, current, updates, writer)) ?? cursor;
|
|
488
|
+
current.state = leanOf(current, result.result.state);
|
|
461
489
|
current.checkpoint = {
|
|
462
490
|
...finished,
|
|
463
491
|
segment: finished.segment + 1,
|
|
@@ -478,16 +506,16 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
478
506
|
const flow = isWorkflowManifest(current.manifest);
|
|
479
507
|
if (!flow) {
|
|
480
508
|
if (result.status !== "failed") {
|
|
481
|
-
const last = await recordTranscript(t, id, s.activeTurnId, current, updates);
|
|
509
|
+
const last = await recordTranscript(t, id, s.activeTurnId, current, updates, writer);
|
|
482
510
|
if (result.status !== "cancelled")
|
|
483
511
|
kept = last ?? cursor;
|
|
484
512
|
}
|
|
485
513
|
current.state =
|
|
486
514
|
result.status === "failed"
|
|
487
515
|
? current.turnStartState
|
|
488
|
-
:
|
|
516
|
+
: leanOf(current, result.result.state);
|
|
489
517
|
}
|
|
490
|
-
current.checkpoint = leanCheckpoint(result.checkpoint);
|
|
518
|
+
current.checkpoint = leanCheckpoint(current, result.checkpoint);
|
|
491
519
|
current.status = result.status;
|
|
492
520
|
current.waits =
|
|
493
521
|
result.status === "paused" ? result.result.pending : undefined;
|
|
@@ -508,7 +536,11 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
508
536
|
});
|
|
509
537
|
siblings.push(...cancelResult.agentSessionIds);
|
|
510
538
|
}
|
|
511
|
-
|
|
539
|
+
// The turn's span (R7.1): a pause starts its wait; an end, its duration and totals.
|
|
540
|
+
const ended = result.status === "paused" ? undefined : await endTurn(t, current, s.activeTurnId);
|
|
541
|
+
if (result.status === "paused")
|
|
542
|
+
pauseTurn(current);
|
|
543
|
+
else {
|
|
512
544
|
current.activeTurnId = null;
|
|
513
545
|
if (s.activeTurnId)
|
|
514
546
|
current.lastTurnId = s.activeTurnId;
|
|
@@ -516,7 +548,7 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
516
548
|
}
|
|
517
549
|
const type = `turn.${result.status}`;
|
|
518
550
|
const payload = result.status === "completed"
|
|
519
|
-
? { output: result.result.output }
|
|
551
|
+
? { output: result.result.output, ...ended }
|
|
520
552
|
: result.status === "paused"
|
|
521
553
|
? {
|
|
522
554
|
interactions: (result.result.pending ?? []).map((call) => ({
|
|
@@ -539,9 +571,10 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
539
571
|
? { path: result.result.error.path }
|
|
540
572
|
: {}),
|
|
541
573
|
},
|
|
574
|
+
...ended,
|
|
542
575
|
}
|
|
543
|
-
: {};
|
|
544
|
-
await t.event(id, s.activeTurnId, type, payload);
|
|
576
|
+
: { ...ended };
|
|
577
|
+
await t.event(id, s.activeTurnId, type, payload, writer);
|
|
545
578
|
await t.put("sessions", id, current);
|
|
546
579
|
await wakeWorkflowOf(ctx, t, current, s.activeTurnId, type, payload);
|
|
547
580
|
return siblings;
|
|
@@ -564,6 +597,37 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
564
597
|
}
|
|
565
598
|
return kept;
|
|
566
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* The approvals a waiting segment's held tool calls ask for (R5 step 2), as `turn.paused`
|
|
602
|
+
* interactions: each one's id is the call's effect id, which `approve` answers.
|
|
603
|
+
*/
|
|
604
|
+
async function heldCalls(t, effectIds) {
|
|
605
|
+
const held = [];
|
|
606
|
+
for (const effectId of effectIds) {
|
|
607
|
+
const row = await t.get("effects", effectId);
|
|
608
|
+
if (row?.status !== "held")
|
|
609
|
+
continue;
|
|
610
|
+
const request = row.request;
|
|
611
|
+
const context = (request.context ?? {});
|
|
612
|
+
const toolName = request.toolName ?? request.key ?? effectId;
|
|
613
|
+
const reason = row.hold?.reason ?? "The Tenant's tool policy asks a person about this call";
|
|
614
|
+
held.push({
|
|
615
|
+
invocationId: typeof context.invocationId === "string" ? context.invocationId : effectId,
|
|
616
|
+
...(typeof context.callId === "string" ? { callId: context.callId } : {}),
|
|
617
|
+
interaction: {
|
|
618
|
+
id: effectId,
|
|
619
|
+
kind: "approval",
|
|
620
|
+
prompt: `Approve ${toolName}?`,
|
|
621
|
+
reason,
|
|
622
|
+
...(row.server === undefined ? {} : { server: row.server }),
|
|
623
|
+
input: request.input ?? null,
|
|
624
|
+
},
|
|
625
|
+
status: "interaction",
|
|
626
|
+
toolName,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return held;
|
|
630
|
+
}
|
|
567
631
|
/** A segment threw: fail the turn, restore the turn-start state, wake a linked workflow. */
|
|
568
632
|
async function settleFailure(ctx, lease, s, error) {
|
|
569
633
|
const id = lease.sessionId;
|
|
@@ -574,7 +638,7 @@ async function settleFailure(ctx, lease, s, error) {
|
|
|
574
638
|
current.status = "failed";
|
|
575
639
|
current.state = current.turnStartState;
|
|
576
640
|
current.error = error instanceof Error ? error.message : String(error);
|
|
577
|
-
const payload = { message: current.error };
|
|
641
|
+
const payload = { message: current.error, ...(await endTurn(t, current, current.activeTurnId)) };
|
|
578
642
|
await t.event(id, current.activeTurnId, "turn.failed", payload);
|
|
579
643
|
await slimModelEffects(t, id, current.activeTurnId);
|
|
580
644
|
if (current.activeTurnId)
|
|
@@ -584,21 +648,30 @@ async function settleFailure(ctx, lease, s, error) {
|
|
|
584
648
|
await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.failed", payload);
|
|
585
649
|
});
|
|
586
650
|
}
|
|
587
|
-
/**
|
|
588
|
-
|
|
651
|
+
/** Whether the session runs our harness, whose state core may store lean (R4.2). */
|
|
652
|
+
const ours = (session) => harnessOf(session) === HARNESS_ID;
|
|
653
|
+
/**
|
|
654
|
+
* `state` as the session row stores it: our engine's without its transcript (`history.ts`);
|
|
655
|
+
* another harness's as it sent it, never read (R4.2).
|
|
656
|
+
*/
|
|
657
|
+
function leanOf(session, state) {
|
|
658
|
+
return ours(session) ? leanState(state) : state;
|
|
659
|
+
}
|
|
660
|
+
/** `checkpoint` with its engine state stored lean (`leanOf`). */
|
|
661
|
+
function leanCheckpoint(session, checkpoint) {
|
|
589
662
|
const state = checkpoint?.state;
|
|
590
663
|
if (!checkpoint || !state)
|
|
591
664
|
return checkpoint;
|
|
592
|
-
return { ...checkpoint, state:
|
|
665
|
+
return { ...checkpoint, state: leanOf(session, state) };
|
|
593
666
|
}
|
|
594
667
|
/**
|
|
595
668
|
* Appends a segment's transcript edits; a snapshot moves `history.snapshot`. Returns the seq of
|
|
596
669
|
* the last edit, if any.
|
|
597
670
|
*/
|
|
598
|
-
async function recordTranscript(t, id, turnId, s, updates) {
|
|
671
|
+
async function recordTranscript(t, id, turnId, s, updates, writer) {
|
|
599
672
|
let last;
|
|
600
673
|
for (const update of updates) {
|
|
601
|
-
const event = await t.event(id, turnId, "transcript.updated", update);
|
|
674
|
+
const event = await t.event(id, turnId, "transcript.updated", update, writer);
|
|
602
675
|
if (update.keep === 0)
|
|
603
676
|
s.history = { from: s.history?.from ?? event.seq, snapshot: event.seq };
|
|
604
677
|
last = event.seq;
|
|
@@ -27,7 +27,7 @@ import { uploadArtifact } from "../artifacts/service.js";
|
|
|
27
27
|
import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
|
|
28
28
|
import { sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
29
29
|
import { sandboxWorkspaceOf } from "../sandbox/share.js";
|
|
30
|
-
import {
|
|
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,10 +265,13 @@ export async function command(ctx, id, input, scope) {
|
|
|
221
265
|
segment: (s.checkpoint?.segment ?? 0) + 1,
|
|
222
266
|
});
|
|
223
267
|
}
|
|
268
|
+
// A paused turn goes on: its wait for a person is not its running time (R7.1).
|
|
269
|
+
resumeTurn(s);
|
|
224
270
|
s.status = "runnable";
|
|
225
271
|
s.waits = undefined;
|
|
226
|
-
// A flow answers each interaction in the same segment
|
|
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 = {
|
|
@@ -232,7 +279,7 @@ export async function command(ctx, id, input, scope) {
|
|
|
232
279
|
dedupeKey: `${command.type}:${s.activeTurnId}:${cause}`,
|
|
233
280
|
};
|
|
234
281
|
await t.wake(id, commandWake);
|
|
235
|
-
event = await t.event(id, s.activeTurnId, `command.${command.type}`, command);
|
|
282
|
+
event = await t.event(id, s.activeTurnId, `command.${command.type}`, command.type === "message" && options.trace ? { ...command, trace: options.trace } : command, commandWriter(scope));
|
|
236
283
|
}
|
|
237
284
|
await t.put("sessions", id, s);
|
|
238
285
|
// Direct cancel of a linked agent fails that node with agent.cancelled (WF-R54). The agent
|
|
@@ -272,3 +319,37 @@ export async function command(ctx, id, input, scope) {
|
|
|
272
319
|
}
|
|
273
320
|
return response;
|
|
274
321
|
}
|
|
322
|
+
/** Core's own commands (R4.9): the flow host's, and a drain's cancels. */
|
|
323
|
+
const CORE_PRINCIPALS = { "flow-host": "flow-host", drain: "core" };
|
|
324
|
+
/**
|
|
325
|
+
* Who sent a command (R4.9), from its credential and never its body: a client over the Runtime
|
|
326
|
+
* API (an application, or a person through it or a token), or core itself.
|
|
327
|
+
*/
|
|
328
|
+
export function commandWriter(scope) {
|
|
329
|
+
if (scope.kind === "application" && CORE_PRINCIPALS[scope.principalId] !== undefined)
|
|
330
|
+
return {
|
|
331
|
+
source: { kind: "core", id: "runtime" },
|
|
332
|
+
principal: { kind: CORE_PRINCIPALS[scope.principalId], id: scope.principalId },
|
|
333
|
+
};
|
|
334
|
+
const principal = scope.kind === "subject" || scope.kind === "token"
|
|
335
|
+
? { kind: "subject", id: scope.subject }
|
|
336
|
+
: { kind: "application", id: scope.kind === "anonymous" ? "anonymous" : scope.principalId };
|
|
337
|
+
return { source: { kind: "client", id: principal.id }, principal };
|
|
338
|
+
}
|
|
339
|
+
/** The held tool call of the session's active turn that `interactionId` names (R5 step 2). */
|
|
340
|
+
async function heldCall(t, s, interactionId) {
|
|
341
|
+
const row = await t.get("effects", interactionId);
|
|
342
|
+
if (row?.status !== "held")
|
|
343
|
+
return undefined;
|
|
344
|
+
if (row.request.sessionId !== s.id || row.request.turnId !== s.activeTurnId)
|
|
345
|
+
return undefined;
|
|
346
|
+
return row;
|
|
347
|
+
}
|
|
348
|
+
/** Who answered: the principal (its subject, when it acts for one), or the token's subject. */
|
|
349
|
+
function principalOf(scope) {
|
|
350
|
+
if (scope.kind === "token")
|
|
351
|
+
return `${scope.issuer}:${scope.subject}`;
|
|
352
|
+
if (scope.kind === "subject")
|
|
353
|
+
return `${scope.principalId}:${scope.subject}`;
|
|
354
|
+
return "principalId" in scope ? scope.principalId : undefined;
|
|
355
|
+
}
|