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