@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
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
* `connection.lost`.
|
|
26
26
|
*/
|
|
27
27
|
import { randomUUID } from "node:crypto";
|
|
28
|
-
import { HARNESS_API_VERSION, HARNESS_CLAIMS, HarnessApiError, } from "@nylorun/core/harness-api";
|
|
28
|
+
import { DEFAULT_HARNESS, HARNESS_API_VERSION, HARNESS_CLAIMS, MAX_HARNESS_STATE_BYTES, isHarnessId, HarnessApiError, } from "@nylorun/core/harness-api";
|
|
29
29
|
import { definitionFilesOf } from "@nylorun/core/contracts";
|
|
30
30
|
import { isOwnershipLost } from "../store/ownership.js";
|
|
31
|
-
import { ownedSession } from "../tenant/context.js";
|
|
31
|
+
import { harnessOf, ownedSession } from "../tenant/context.js";
|
|
32
32
|
import { mcpDiscovered, pinDeferral, sessionToolsOf, } from "../mcp/snapshot.js";
|
|
33
33
|
import { contextWindowOf } from "../model/catalog.js";
|
|
34
34
|
import { turnManifestOf } from "../tenant/session.js";
|
|
@@ -36,6 +36,7 @@ import { sandboxWorkspaceKey, workspacePrefix } from "../sandbox/records.js";
|
|
|
36
36
|
import { abortKind } from "../tenant/worker.js";
|
|
37
37
|
import { readDefinitionFile } from "../tenant/definition-files.js";
|
|
38
38
|
import { recordIntent, recordOutcome } from "./record.js";
|
|
39
|
+
import { redactTokens } from "./redact.js";
|
|
39
40
|
import { beat, renewEveryMs, tokenOf } from "./renew.js";
|
|
40
41
|
/** No attached harness serves workspaces (or, with `pod`, that pod sandbox's). */
|
|
41
42
|
export class NoWorkspaceHarness extends Error {
|
|
@@ -48,6 +49,8 @@ export class NoWorkspaceHarness extends Error {
|
|
|
48
49
|
this.pod = pod;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
/** What our harness said in `hello` before harness ids (R4.1): refused, with the new id. */
|
|
53
|
+
const LEGACY_NAME = "@nylorun/harness";
|
|
51
54
|
const OUTPUTS = [
|
|
52
55
|
"turn.completed",
|
|
53
56
|
"turn.paused",
|
|
@@ -62,8 +65,11 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
62
65
|
const queued = [];
|
|
63
66
|
const runs = new Map();
|
|
64
67
|
let closed = false;
|
|
65
|
-
/**
|
|
66
|
-
|
|
68
|
+
/**
|
|
69
|
+
* A harness may take a run of its own harness id (R4.1): a pod sandbox's host its sandbox's
|
|
70
|
+
* runs, any other the rest.
|
|
71
|
+
*/
|
|
72
|
+
const takes = (connection, run) => connection.harness === (run.offer.harness ?? DEFAULT_HARNESS) && connection.host?.sandboxId === run.offer.pod;
|
|
67
73
|
/** Hands queued runs to waiting harnesses that may take them, in order. */
|
|
68
74
|
const match = () => {
|
|
69
75
|
for (let i = 0; i < queued.length;) {
|
|
@@ -89,16 +95,30 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
89
95
|
throw new HarnessApiError("run_not_held", `Run ${runId} is not held by this harness`);
|
|
90
96
|
return run;
|
|
91
97
|
};
|
|
98
|
+
/** What a harness reports is its claim (R4.9): its harness id and, in a run, the run. */
|
|
99
|
+
const writerOf = (connection, runId) => ({
|
|
100
|
+
source: { kind: "harness", id: connection.harness ?? connection.peer.name },
|
|
101
|
+
evidence: "claimed",
|
|
102
|
+
...(runId === undefined ? {} : { runId }),
|
|
103
|
+
});
|
|
92
104
|
const scope = (run) => ({
|
|
93
105
|
ctx,
|
|
94
106
|
lease: run.offer.lease,
|
|
95
107
|
signal: run.offer.controller.signal,
|
|
108
|
+
...(run.connection?.tools ? { nativeTools: run.connection.tools } : {}),
|
|
109
|
+
...(run.connection ? { writer: writerOf(run.connection, run.grant.runId) } : {}),
|
|
96
110
|
});
|
|
97
111
|
const serve = (connection) => async (method, params, signal) => {
|
|
98
112
|
const p = params;
|
|
99
113
|
if (method === "hello") {
|
|
100
|
-
const { capabilities } = p;
|
|
114
|
+
const { api, name, capabilities } = p;
|
|
115
|
+
// R3's next version adds a window of supported versions; until then there is one.
|
|
116
|
+
if (api !== HARNESS_API_VERSION)
|
|
117
|
+
throw new HarnessApiError("invalid", `unsupported Harness API version ${api}: this Runtime speaks ${HARNESS_API_VERSION}`);
|
|
118
|
+
connection.harness = await harnessIdOf(connection, name);
|
|
101
119
|
connection.workspace = capabilities.workspace !== undefined;
|
|
120
|
+
if (capabilities.tools?.length)
|
|
121
|
+
connection.tools = new Set(capabilities.tools.map((tool) => tool.name));
|
|
102
122
|
return {
|
|
103
123
|
api: HARNESS_API_VERSION,
|
|
104
124
|
tenantId: ctx.config.tenantId,
|
|
@@ -109,6 +129,8 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
109
129
|
}
|
|
110
130
|
if (method === "event")
|
|
111
131
|
return claim(connection, p);
|
|
132
|
+
if (method === "lease" && connection.harness === undefined)
|
|
133
|
+
throw new HarnessApiError("invalid", "Say hello first: a harness leases runs of the id it names");
|
|
112
134
|
if (method === "lease")
|
|
113
135
|
return new Promise((resolve, reject) => {
|
|
114
136
|
const entry = { connection, resolve };
|
|
@@ -121,8 +143,12 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
121
143
|
});
|
|
122
144
|
match();
|
|
123
145
|
});
|
|
124
|
-
if (OUTPUTS.includes(method))
|
|
125
|
-
|
|
146
|
+
if (OUTPUTS.includes(method)) {
|
|
147
|
+
const run = held(connection, String(p.runId), method);
|
|
148
|
+
if (connection.harness !== DEFAULT_HARNESS)
|
|
149
|
+
checkCustomOutput(run, method, p);
|
|
150
|
+
return output(run, method, p);
|
|
151
|
+
}
|
|
126
152
|
const run = held(connection, String(p.runId));
|
|
127
153
|
switch (method) {
|
|
128
154
|
case "lease.renew":
|
|
@@ -150,6 +176,27 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
150
176
|
throw new HarnessApiError("invalid", `Unknown request ${method}`);
|
|
151
177
|
}
|
|
152
178
|
};
|
|
179
|
+
/**
|
|
180
|
+
* The harness id a `hello` names (R4.1): a harness id, and the one the connection's credential
|
|
181
|
+
* is for. A pod's host must be its Environment's harness, so an image that is not the harness
|
|
182
|
+
* its recipe names never takes the Environment's runs.
|
|
183
|
+
*/
|
|
184
|
+
const harnessIdOf = async (connection, name) => {
|
|
185
|
+
if (!isHarnessId(name))
|
|
186
|
+
throw new HarnessApiError("invalid", name === LEGACY_NAME
|
|
187
|
+
? `A harness names its harness id in hello: ours is ${DEFAULT_HARNESS}. Update the harness image (it said ${LEGACY_NAME}).`
|
|
188
|
+
: `hello.name ${JSON.stringify(name)} is not a harness id: lowercase letters, digits and dashes, starting with a letter`);
|
|
189
|
+
const bound = connection.peer.harness;
|
|
190
|
+
if (bound !== undefined && bound !== name)
|
|
191
|
+
throw new HarnessApiError("invalid", `This credential is for the ${bound} harness, not ${name}`);
|
|
192
|
+
if (connection.host) {
|
|
193
|
+
const environment = await ctx.store.tx((t) => t.environment(connection.host.sandboxId));
|
|
194
|
+
const expected = environment?.applied.harness ?? DEFAULT_HARNESS;
|
|
195
|
+
if (expected !== name)
|
|
196
|
+
throw new HarnessApiError("invalid", `Environment ${connection.host.sandboxId} runs the ${expected} harness, and this pod says ${name}`);
|
|
197
|
+
}
|
|
198
|
+
return name;
|
|
199
|
+
};
|
|
153
200
|
/**
|
|
154
201
|
* A `sandbox.*` event the harness claims: for a run it holds (the run's session, or the
|
|
155
202
|
* session owning its sandbox), or a session one of its workspace requests acts for. A
|
|
@@ -169,7 +216,7 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
169
216
|
throw new HarnessApiError("run_not_held", `This harness may not claim events of ${event.sessionId}`);
|
|
170
217
|
const record = event.record;
|
|
171
218
|
if (record &&
|
|
172
|
-
(event.type !== "
|
|
219
|
+
(event.type !== "environment.state" ||
|
|
173
220
|
record.sessionId !== event.sessionId ||
|
|
174
221
|
!record.key.startsWith(workspacePrefix(ctx.config.tenantId)) ||
|
|
175
222
|
(connection.host !== undefined &&
|
|
@@ -179,7 +226,7 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
179
226
|
await ctx.store.tx(async (t) => {
|
|
180
227
|
if (record)
|
|
181
228
|
await t.put("sandboxes", record.key, record);
|
|
182
|
-
await t.event(event.sessionId, event.turnId, event.type, event.payload);
|
|
229
|
+
await t.event(event.sessionId, event.turnId, event.type, redactTokens(event.payload), writerOf(connection, event.runId));
|
|
183
230
|
});
|
|
184
231
|
}
|
|
185
232
|
catch (error) {
|
|
@@ -211,7 +258,7 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
211
258
|
current.mcpSnapshot = pinDeferral(snapshot, current.manifest, contextWindow);
|
|
212
259
|
current.mcpDiagnostics = diagnostics;
|
|
213
260
|
await t.put("sessions", current.id, current);
|
|
214
|
-
await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(current.mcpSnapshot, diagnostics));
|
|
261
|
+
await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(current.mcpSnapshot, diagnostics), run.connection ? writerOf(run.connection, run.grant.runId) : undefined);
|
|
215
262
|
}
|
|
216
263
|
else if (diagnostics.length > 0) {
|
|
217
264
|
const prior = [...(current.mcpDiagnostics ?? [])];
|
|
@@ -241,6 +288,27 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
241
288
|
throw new HarnessApiError("unavailable", `The Object store has no bytes for ${sha256}`);
|
|
242
289
|
return { base64: Buffer.from(bytes).toString("base64") };
|
|
243
290
|
};
|
|
291
|
+
/**
|
|
292
|
+
* A custom harness's output (R4.2): its state is its own, stored as sent and never read, so it
|
|
293
|
+
* must be JSON and at most `MAX_HARNESS_STATE_BYTES`; a larger one fails the turn, as an engine
|
|
294
|
+
* that threw would. It cannot pause for approvals in v2 (Q14): approvals read our engine's plan
|
|
295
|
+
* until R5 makes them core holds.
|
|
296
|
+
*/
|
|
297
|
+
const checkCustomOutput = (run, method, params) => {
|
|
298
|
+
if (method === "turn.paused")
|
|
299
|
+
throw new HarnessApiError("invalid", "A custom harness cannot pause for approvals in Harness API 2");
|
|
300
|
+
if (params.state === undefined || run.outputs.has(method))
|
|
301
|
+
return;
|
|
302
|
+
const bytes = Buffer.byteLength(JSON.stringify(params.state) ?? "");
|
|
303
|
+
if (bytes <= MAX_HARNESS_STATE_BYTES)
|
|
304
|
+
return;
|
|
305
|
+
output(run, "turn.failed", {
|
|
306
|
+
runId: run.grant.runId,
|
|
307
|
+
status: "failed",
|
|
308
|
+
thrown: { code: "invalid", message: `The harness's state is ${bytes} bytes, over ${MAX_HARNESS_STATE_BYTES}` },
|
|
309
|
+
}).catch(() => undefined);
|
|
310
|
+
throw new HarnessApiError("invalid", `state over 256 KiB (${bytes} bytes): the turn failed`);
|
|
311
|
+
};
|
|
244
312
|
const output = (run, method, params) => {
|
|
245
313
|
const known = run.outputs.get(method);
|
|
246
314
|
if (known)
|
|
@@ -262,7 +330,10 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
262
330
|
status() {
|
|
263
331
|
return {
|
|
264
332
|
connected: connections.size,
|
|
265
|
-
workspace: [...connections].some((connection) => connection.workspace &&
|
|
333
|
+
workspace: [...connections].some((connection) => connection.workspace &&
|
|
334
|
+
!connection.host &&
|
|
335
|
+
connection.harness === DEFAULT_HARNESS &&
|
|
336
|
+
!connection.channel.closed),
|
|
266
337
|
};
|
|
267
338
|
},
|
|
268
339
|
hosting(sandboxId) {
|
|
@@ -275,10 +346,19 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
275
346
|
},
|
|
276
347
|
async workspace(method, params, signal, target) {
|
|
277
348
|
const pod = target?.pod;
|
|
278
|
-
const
|
|
349
|
+
const session = params.session?.ownerId;
|
|
350
|
+
// A pod's workspace is its host's, whatever harness it is. Any other session's is served
|
|
351
|
+
// by its own harness, if that harness serves workspaces: our harness's workspaces are not
|
|
352
|
+
// a custom harness's files (R4.1). Sweeps and removes go to ours.
|
|
353
|
+
const harness = pod !== undefined || session === undefined
|
|
354
|
+
? DEFAULT_HARNESS
|
|
355
|
+
: harnessOf((await ctx.store.tx((t) => t.get("sessions", session))) ?? {});
|
|
356
|
+
const connection = [...connections].find((item) => item.workspace &&
|
|
357
|
+
!item.channel.closed &&
|
|
358
|
+
item.host?.sandboxId === pod &&
|
|
359
|
+
(pod !== undefined || item.harness === harness));
|
|
279
360
|
if (!connection)
|
|
280
361
|
throw new NoWorkspaceHarness(pod);
|
|
281
|
-
const session = params.session?.ownerId;
|
|
282
362
|
if (session)
|
|
283
363
|
connection.claims.set(session, (connection.claims.get(session) ?? 0) + 1);
|
|
284
364
|
else
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RecordedOutcome, TurnStart } from "@nylorun/core/harness-api";
|
|
2
2
|
import type { SandboxPodState } from "../store/types.js";
|
|
3
3
|
import { type Lease, type Session, type TenantContext } from "../tenant/context.js";
|
|
4
|
+
import { type TraceContext } from "../traces/ids.js";
|
|
4
5
|
/** A segment ready to run: the session as it starts, and its transcript folded from the record. */
|
|
5
6
|
export interface SegmentStart {
|
|
6
7
|
readonly current: Session;
|
|
@@ -10,18 +11,24 @@ export interface SegmentStart {
|
|
|
10
11
|
/** The record position of `transcript` (`foldTranscriptWithCursor`). */
|
|
11
12
|
readonly cursor: number;
|
|
12
13
|
readonly outcomes: RecordedOutcome[];
|
|
13
|
-
/** The
|
|
14
|
+
/** The session's sandbox workspace: the session, and the Environment whose workspace it is. */
|
|
14
15
|
readonly sandbox: {
|
|
15
16
|
readonly ownerId: string;
|
|
16
|
-
readonly
|
|
17
|
+
readonly environmentId?: string;
|
|
17
18
|
/** Variables every sandbox command gets (R2c): secret names set to the sentinel, plain values. */
|
|
18
19
|
readonly environment?: Readonly<Record<string, string>>;
|
|
19
20
|
};
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* The Environment is a pod (F7.2): its engine runs the segment. Its lifecycle state, and
|
|
23
|
+
* whether its new volume is still being seeded from a disk version (R2d step 3).
|
|
24
|
+
*/
|
|
21
25
|
readonly pod?: {
|
|
22
26
|
readonly id: string;
|
|
23
27
|
readonly pod: SandboxPodState;
|
|
28
|
+
readonly seeding?: boolean;
|
|
24
29
|
};
|
|
30
|
+
/** The turn's span context (R7.4): what `TurnStart.trace` hands the harness. */
|
|
31
|
+
readonly trace?: TraceContext;
|
|
25
32
|
}
|
|
26
33
|
/** The segment's starting transaction and fold. */
|
|
27
34
|
export declare function startSegment(ctx: TenantContext, lease: Lease,
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { isWorkflowManifest } from "../core/flow-host.js";
|
|
2
2
|
import { sandboxWorkspaceOf } from "../sandbox/share.js";
|
|
3
3
|
import { ownedTx } from "../store/ownership.js";
|
|
4
|
-
import {
|
|
4
|
+
import { harnessOf } from "../tenant/context.js";
|
|
5
|
+
import { HARNESS_ID } from "../sandbox/placement.js";
|
|
5
6
|
import { checkParity, foldTranscriptWithCursor, leanState, transcriptOf, transcriptUpdates, withTranscript, } from "../tenant/history.js";
|
|
6
7
|
import { usesFixtureModel } from "../tenant/model-setting.js";
|
|
7
|
-
import {
|
|
8
|
+
import { sessionToolsOf, turnManifestOf } from "../tenant/session.js";
|
|
8
9
|
import { bulkOutcomes } from "./record.js";
|
|
10
|
+
import { traceKey } from "../traces/key.js";
|
|
11
|
+
import { traceparentOf, turnSpanOf, turnTraceIdOf, } from "../traces/ids.js";
|
|
9
12
|
/** Segment rollover defaults (Model Calls §10), well inside the advance deadline. */
|
|
10
13
|
const ROLLOVER_STEPS = 50;
|
|
11
14
|
const ROLLOVER_MS = 20 * 60_000;
|
|
@@ -14,9 +17,12 @@ export async function startSegment(ctx, lease,
|
|
|
14
17
|
/** For a harness: also the segment's outcomes and the sandbox's owner. */
|
|
15
18
|
options = {}) {
|
|
16
19
|
const id = lease.sessionId;
|
|
20
|
+
// Read before the transaction: the first read of the Tenant may create the key.
|
|
21
|
+
const key = options.harness ? await traceKey(ctx.store) : undefined;
|
|
17
22
|
const started = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
// A transcript left on our engine's state by an older Runtime is recorded once; another
|
|
24
|
+
// harness's state is its own (R4.2).
|
|
25
|
+
if (!isWorkflowManifest(current.manifest) && current.checkpoint && harnessOf(current) === HARNESS_ID) {
|
|
20
26
|
await adoptStoredTranscript(t, current);
|
|
21
27
|
const cp = current.checkpoint;
|
|
22
28
|
current.checkpoint = { ...cp, state: current.state };
|
|
@@ -25,22 +31,33 @@ options = {}) {
|
|
|
25
31
|
const fixtureModel = await usesFixtureModel(t);
|
|
26
32
|
if (!options.harness)
|
|
27
33
|
return { current, fixtureModel, outcomes: [], sandbox: { ownerId: id } };
|
|
28
|
-
const workspace = sandboxWorkspaceOf(current
|
|
29
|
-
const resource = workspace.
|
|
30
|
-
const
|
|
34
|
+
const workspace = sandboxWorkspaceOf(current);
|
|
35
|
+
const resource = workspace.environmentId === undefined ? undefined : await t.environment(workspace.environmentId);
|
|
36
|
+
const environment = await ctx.vault.environmentVariables(t, current.environmentId);
|
|
31
37
|
const sandbox = Object.keys(environment).length > 0 ? { ...workspace, environment } : workspace;
|
|
38
|
+
const turnId = current.activeTurnId;
|
|
32
39
|
return {
|
|
33
40
|
current,
|
|
34
41
|
fixtureModel,
|
|
35
42
|
outcomes: current.checkpoint ? await bulkOutcomes(t, current, current.checkpoint) : [],
|
|
36
43
|
sandbox,
|
|
37
|
-
...(
|
|
44
|
+
...(key && turnId ? { trace: turnContextOf(key, ctx.store.tenantId, current, turnId) } : {}),
|
|
45
|
+
...(resource?.pod
|
|
46
|
+
? { pod: { id: resource.id, pod: resource.pod, ...(resource.diskSeed ? { seeding: true } : {}) } }
|
|
47
|
+
: {}),
|
|
38
48
|
};
|
|
39
49
|
});
|
|
40
50
|
// One history (P0.3): the engine resumes from the transcript folded from the record.
|
|
41
51
|
const { current } = started;
|
|
42
52
|
const cp = current.checkpoint;
|
|
43
|
-
if (isWorkflowManifest(current.manifest)
|
|
53
|
+
if (isWorkflowManifest(current.manifest))
|
|
54
|
+
return { ...started, transcript: [], cursor: -1 };
|
|
55
|
+
// Another harness's state says nothing about its transcript (R4.2): fold it every segment.
|
|
56
|
+
if (harnessOf(current) !== HARNESS_ID) {
|
|
57
|
+
const { transcript, cursor } = await foldSession(ctx, current);
|
|
58
|
+
return { ...started, transcript, cursor };
|
|
59
|
+
}
|
|
60
|
+
if (!cp?.state)
|
|
44
61
|
return { ...started, transcript: [], cursor: -1 };
|
|
45
62
|
const { transcript, cursor } = await foldSession(ctx, current);
|
|
46
63
|
checkParity(id, transcript, transcriptOf(cp.state));
|
|
@@ -56,7 +73,7 @@ export function buildTurnStart(ctx, segment) {
|
|
|
56
73
|
type: !flow && (input?.kind === "approve" || input?.kind === "respond") ? "approval.answer" : "turn.start",
|
|
57
74
|
engine: flow ? "flow" : "agent",
|
|
58
75
|
manifest: flow ? current.manifest : turnManifestOf(current),
|
|
59
|
-
checkpoint: !flow && cp.state
|
|
76
|
+
checkpoint: !flow && cp.state && harnessOf(current) === HARNESS_ID
|
|
60
77
|
? { ...cp, state: withTranscript(cp.state, []) }
|
|
61
78
|
: cp,
|
|
62
79
|
// The turn's manifest: a variant may have disabled MCP tools or required their approval.
|
|
@@ -74,6 +91,19 @@ export function buildTurnStart(ctx, segment) {
|
|
|
74
91
|
...(current.mcpSnapshot ? { mcpSnapshot: current.mcpSnapshot } : {}),
|
|
75
92
|
sandbox: segment.sandbox,
|
|
76
93
|
},
|
|
94
|
+
...(segment.trace ? { trace: segment.trace } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The context of turn `turnId`'s span (R7.4): its trace (the caller's when its message carried a
|
|
99
|
+
* `traceparent`) and its span, as the trace projector derives them, with the caller's
|
|
100
|
+
* `tracestate`.
|
|
101
|
+
*/
|
|
102
|
+
function turnContextOf(key, tenantId, s, turnId) {
|
|
103
|
+
const traceId = turnTraceIdOf(key, { tenantId, sessionId: s.id, turnId }, s.turnTrace);
|
|
104
|
+
return {
|
|
105
|
+
traceparent: traceparentOf(traceId, turnSpanOf(key, turnId)),
|
|
106
|
+
...(s.turnTrace?.tracestate ? { tracestate: s.turnTrace.tracestate } : {}),
|
|
77
107
|
};
|
|
78
108
|
}
|
|
79
109
|
/** The engine's yield budget for a segment. */
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The effect journal's tool calls (R5): every tool call is a row of `nylorun.effects`, and how
|
|
3
|
+
* it ends is one `tool.completed` with its status, kind, server, attempts and duration (R5.1).
|
|
4
|
+
*
|
|
5
|
+
* Delivery (R5.3, R5.4). A tool call is sent at least once, and every send carries its effect
|
|
6
|
+
* id as the key. After a crash, a shutdown or a lost gate hop, a row still `invoking` is sent
|
|
7
|
+
* again: an MCP or HTTP tool call (it crosses the Tool Gate, which hands the tool the key) up to
|
|
8
|
+
* `MAX_TOOL_ATTEMPTS` sends, then it fails to the model as `tool.interrupted`. A sandbox or
|
|
9
|
+
* core-served call has no key to dedupe on, so a crash fails it to the model at once, and
|
|
10
|
+
* nothing runs it again (Q2). No tool call leaves its session `uncertain`.
|
|
11
|
+
*
|
|
12
|
+
* The row body keeps the journal's facts: `toolKind`, `server`, `attempts`, `startedAt` and
|
|
13
|
+
* `settledAt`. A row written before R5 has none of them.
|
|
14
|
+
*
|
|
15
|
+
* Spans (R7.1). `tool.completed` also carries the call's effect id, the Tool Gate's own start
|
|
16
|
+
* and end of its last send (`gate`, from its `tool_crossings` row: MCP and HTTP calls through
|
|
17
|
+
* the gateway, while the row is kept), and what the tool policy did when it did more than allow
|
|
18
|
+
* (`policy`). A flow's HTTP stage, which names no tool call, ends in `node.completed` instead.
|
|
19
|
+
*/
|
|
20
|
+
import type { EventWriter } from "@nylorun/core/contracts";
|
|
21
|
+
import type { AgentManifest } from "@nylorun/core/define";
|
|
22
|
+
import type { HostEffect } from "@nylorun/harness/run";
|
|
23
|
+
import type { Session } from "../tenant/context.js";
|
|
24
|
+
import { type GateTimes, type ToolCallFacts, type ToolCallKind, type ToolCallStatus, type ToolPolicyFact } from "../tenant/transcript.js";
|
|
25
|
+
import type { EffectDoc, Tx } from "../store/types.js";
|
|
26
|
+
/** How many times a tool call is sent before it fails to the model as `tool.interrupted`. */
|
|
27
|
+
export declare const MAX_TOOL_ATTEMPTS = 3;
|
|
28
|
+
/** A tool row's journal facts (R5), kept in its body. */
|
|
29
|
+
export interface ToolRowFacts {
|
|
30
|
+
/** Why the Tool Gate held the call for approval, and since when (R5 step 2). */
|
|
31
|
+
hold?: {
|
|
32
|
+
reason: string;
|
|
33
|
+
at: string;
|
|
34
|
+
};
|
|
35
|
+
/** A person's approval of a held call (R5 step 2): the gate then sends it. */
|
|
36
|
+
approval?: {
|
|
37
|
+
approved: true;
|
|
38
|
+
at: string;
|
|
39
|
+
by?: string;
|
|
40
|
+
};
|
|
41
|
+
toolKind?: ToolCallKind;
|
|
42
|
+
server?: string;
|
|
43
|
+
attempts?: number;
|
|
44
|
+
startedAt?: string;
|
|
45
|
+
settledAt?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The tool span's context (R7.4), `00-<trace>-<span>-01`: the Tool Gate reads it with the row
|
|
48
|
+
* and sends it as `traceparent`, and a harness hands it to a sandbox command as `TRACEPARENT`.
|
|
49
|
+
*/
|
|
50
|
+
traceparent?: string;
|
|
51
|
+
}
|
|
52
|
+
export type ToolRow = EffectDoc & ToolRowFacts & {
|
|
53
|
+
requestHash?: string;
|
|
54
|
+
outcome?: {
|
|
55
|
+
value: unknown;
|
|
56
|
+
statePatch?: Record<string, unknown>;
|
|
57
|
+
};
|
|
58
|
+
error?: string;
|
|
59
|
+
};
|
|
60
|
+
/** Where a tool call runs, and its MCP server or HTTP tool capability. */
|
|
61
|
+
export declare function toolKindOf(s: Pick<Session, "manifest" | "mcpSnapshot">, agentManifest: AgentManifest | undefined, request: HostEffect): {
|
|
62
|
+
toolKind: ToolCallKind;
|
|
63
|
+
server?: string;
|
|
64
|
+
};
|
|
65
|
+
/** The facts a new tool row starts with: its first send, now. */
|
|
66
|
+
export declare function firstSend(kind: {
|
|
67
|
+
toolKind: ToolCallKind;
|
|
68
|
+
server?: string;
|
|
69
|
+
}, now?: Date): ToolRowFacts;
|
|
70
|
+
/** A failed outcome the Runtime gives a call a crash interrupted: it may have run. */
|
|
71
|
+
export declare function interrupted(message: string): {
|
|
72
|
+
kind: string;
|
|
73
|
+
code: string;
|
|
74
|
+
message: string;
|
|
75
|
+
retryable: boolean;
|
|
76
|
+
};
|
|
77
|
+
/** The facts `tool.completed` carries for `row`, settled with `status`, and the gate's times. */
|
|
78
|
+
export declare function factsOf(row: ToolRow, status: ToolCallStatus, gate?: GateTimes): ToolCallFacts;
|
|
79
|
+
/**
|
|
80
|
+
* What the Tenant's tool policy did with `row` (R5 step 2), when it did more than allow it: held
|
|
81
|
+
* it for a person (`ask`, with who approved it, when someone did), or refused it unsent (`deny`).
|
|
82
|
+
*/
|
|
83
|
+
export declare function policyOf(row: ToolRow): ToolPolicyFact | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Settles a tool row with `value` (`completed`) or as `cancelled`, and writes its one
|
|
86
|
+
* `tool.completed` (an agent's tool call), or `node.completed` (a flow's HTTP stage or verifier,
|
|
87
|
+
* which names no call, R7.1). Writes `row`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function settleToolRow(t: Tx, s: Pick<Session, "id">, row: ToolRow, value: unknown, options?: {
|
|
90
|
+
cancelled?: boolean;
|
|
91
|
+
now?: Date;
|
|
92
|
+
/** Who reported the outcome (R4.9): the harness that ran the call; absent, core. */
|
|
93
|
+
writer?: EventWriter;
|
|
94
|
+
}): Promise<{
|
|
95
|
+
value: unknown;
|
|
96
|
+
}>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { isFlowToolEffect } from "../core/flow-host.js";
|
|
2
|
+
import { isHttpToolCall } from "../gates/http-tool.js";
|
|
3
|
+
import { mcpToolOf } from "../mcp/snapshot.js";
|
|
4
|
+
import { sandboxCapabilityOf } from "../sandbox/capability.js";
|
|
5
|
+
import { toolCompleted, } from "../tenant/transcript.js";
|
|
6
|
+
import { toolCallDuration, toolCallStatus } from "./tool-status.js";
|
|
7
|
+
/** How many times a tool call is sent before it fails to the model as `tool.interrupted`. */
|
|
8
|
+
export const MAX_TOOL_ATTEMPTS = 3;
|
|
9
|
+
/** Where a tool call runs, and its MCP server or HTTP tool capability. */
|
|
10
|
+
export function toolKindOf(s, agentManifest, request) {
|
|
11
|
+
const mcp = mcpToolOf(s.mcpSnapshot, request);
|
|
12
|
+
if (mcp)
|
|
13
|
+
return { toolKind: "mcp", server: mcp.serverName };
|
|
14
|
+
if (isHttpToolCall(s.manifest, request))
|
|
15
|
+
return { toolKind: "http", ...(request.capabilityId ? { server: request.capabilityId } : {}) };
|
|
16
|
+
if (sandboxCapabilityOf(agentManifest, request.capabilityId, request.toolName))
|
|
17
|
+
return { toolKind: "sandbox" };
|
|
18
|
+
return { toolKind: "platform" };
|
|
19
|
+
}
|
|
20
|
+
/** The facts a new tool row starts with: its first send, now. */
|
|
21
|
+
export function firstSend(kind, now = new Date()) {
|
|
22
|
+
return { ...kind, attempts: 1, startedAt: now.toISOString() };
|
|
23
|
+
}
|
|
24
|
+
/** A failed outcome the Runtime gives a call a crash interrupted: it may have run. */
|
|
25
|
+
export function interrupted(message) {
|
|
26
|
+
return { kind: "failed", code: "tool.interrupted", message, retryable: false };
|
|
27
|
+
}
|
|
28
|
+
/** The facts `tool.completed` carries for `row`, settled with `status`, and the gate's times. */
|
|
29
|
+
export function factsOf(row, status, gate) {
|
|
30
|
+
const durationMs = toolCallDuration(row);
|
|
31
|
+
const policy = policyOf(row);
|
|
32
|
+
return {
|
|
33
|
+
status,
|
|
34
|
+
...(row.toolKind === undefined ? {} : { kind: row.toolKind }),
|
|
35
|
+
...(row.server === undefined ? {} : { server: row.server }),
|
|
36
|
+
...(row.attempts === undefined ? {} : { attempts: row.attempts }),
|
|
37
|
+
...(durationMs === undefined ? {} : { durationMs }),
|
|
38
|
+
effectId: row.request.effectId,
|
|
39
|
+
...(gate === undefined ? {} : { gate }),
|
|
40
|
+
...(policy === undefined ? {} : { policy }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What the Tenant's tool policy did with `row` (R5 step 2), when it did more than allow it: held
|
|
45
|
+
* it for a person (`ask`, with who approved it, when someone did), or refused it unsent (`deny`).
|
|
46
|
+
*/
|
|
47
|
+
export function policyOf(row) {
|
|
48
|
+
if (row.hold)
|
|
49
|
+
return {
|
|
50
|
+
decision: "ask",
|
|
51
|
+
reason: row.hold.reason,
|
|
52
|
+
...(row.approval?.by === undefined ? {} : { approvedBy: row.approval.by }),
|
|
53
|
+
};
|
|
54
|
+
const value = row.outcome?.value;
|
|
55
|
+
// `bypolicy` refuses a call before its first send.
|
|
56
|
+
if (row.attempts === 0 && value?.code === "tool.denied")
|
|
57
|
+
return { decision: "deny", reason: String(value.message ?? "") };
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The Tool Gate's own start and end of `row`'s last send (R7.1), from its `tool_crossings` row:
|
|
62
|
+
* an MCP or HTTP call the gateway ran and settled. Undefined for any other call, one never sent,
|
|
63
|
+
* and one whose crossing is gone (pruned a day after it settled) or not settled.
|
|
64
|
+
*/
|
|
65
|
+
async function gateOf(t, row) {
|
|
66
|
+
if ((row.toolKind !== "mcp" && row.toolKind !== "http") || !row.attempts)
|
|
67
|
+
return undefined;
|
|
68
|
+
const crossing = await t.toolCrossing(row.request.effectId);
|
|
69
|
+
return crossing?.settledAt ? { startedAt: crossing.startedAt, endedAt: crossing.settledAt } : undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Settles a tool row with `value` (`completed`) or as `cancelled`, and writes its one
|
|
73
|
+
* `tool.completed` (an agent's tool call), or `node.completed` (a flow's HTTP stage or verifier,
|
|
74
|
+
* which names no call, R7.1). Writes `row`.
|
|
75
|
+
*/
|
|
76
|
+
export async function settleToolRow(t, s, row, value, options = {}) {
|
|
77
|
+
const request = row.request;
|
|
78
|
+
row.status = options.cancelled ? "cancelled" : "completed";
|
|
79
|
+
row.outcome = { value };
|
|
80
|
+
row.settledAt = (options.now ?? new Date()).toISOString();
|
|
81
|
+
await t.put("effects", request.effectId, row);
|
|
82
|
+
const status = toolCallStatus(row);
|
|
83
|
+
const gate = await gateOf(t, row);
|
|
84
|
+
if (isFlowToolEffect(request)) {
|
|
85
|
+
const durationMs = toolCallDuration(row);
|
|
86
|
+
await t.event(s.id, request.turnId, "node.completed", {
|
|
87
|
+
path: request.path,
|
|
88
|
+
kind: row.toolKind ?? "http",
|
|
89
|
+
key: request.key,
|
|
90
|
+
...(request.iterations !== undefined ? { iterations: request.iterations } : {}),
|
|
91
|
+
effectId: request.effectId,
|
|
92
|
+
status,
|
|
93
|
+
...(durationMs === undefined ? {} : { durationMs }),
|
|
94
|
+
...(gate === undefined ? {} : { gate }),
|
|
95
|
+
}, options.writer);
|
|
96
|
+
return row.outcome;
|
|
97
|
+
}
|
|
98
|
+
const transcript = toolCompleted(request, value, factsOf(row, status, gate));
|
|
99
|
+
if (transcript)
|
|
100
|
+
await t.event(s.id, request.turnId, "tool.completed", transcript, options.writer);
|
|
101
|
+
return row.outcome;
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** What a tool row's body says of how it ended. */
|
|
2
|
+
export interface ToolRowState {
|
|
3
|
+
readonly status: string;
|
|
4
|
+
readonly outcome?: {
|
|
5
|
+
readonly value: unknown;
|
|
6
|
+
};
|
|
7
|
+
readonly startedAt?: string;
|
|
8
|
+
readonly settledAt?: string;
|
|
9
|
+
}
|
|
10
|
+
/** How a tool row ended, as `tool.completed` and `/calls/tool` say it. */
|
|
11
|
+
export declare function toolCallStatus(row: ToolRowState): "completed" | "failed" | "cancelled" | "invoking" | "held" | "uncertain";
|
|
12
|
+
/** From the first send to the outcome, or undefined when the call never ran or has not ended. */
|
|
13
|
+
export declare function toolCallDuration(row: ToolRowState): number | undefined;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a tool row of the effect journal reads (R5.1, R5.2), from its body alone: shared by the
|
|
3
|
+
* journal (`tool-journal.ts`) and the session reads (`/calls/tool`).
|
|
4
|
+
*/
|
|
5
|
+
import { durationBetween } from "../tenant/transcript.js";
|
|
6
|
+
/** The codes of a call the Runtime ended rather than the tool: its status is `failed`. */
|
|
7
|
+
const RUNTIME_ENDED = new Set(["tool.disabled", "tool.unavailable", "tool.interrupted", "tool.denied"]);
|
|
8
|
+
/** How a tool row ended, as `tool.completed` and `/calls/tool` say it. */
|
|
9
|
+
export function toolCallStatus(row) {
|
|
10
|
+
if (row.status === "cancelled")
|
|
11
|
+
return "cancelled";
|
|
12
|
+
if (row.status === "held")
|
|
13
|
+
return "held";
|
|
14
|
+
if (row.status === "uncertain")
|
|
15
|
+
return "uncertain";
|
|
16
|
+
if (row.status !== "completed")
|
|
17
|
+
return "invoking";
|
|
18
|
+
const value = row.outcome?.value;
|
|
19
|
+
return value?.kind === "failed" && typeof value.code === "string" && RUNTIME_ENDED.has(value.code)
|
|
20
|
+
? "failed"
|
|
21
|
+
: "completed";
|
|
22
|
+
}
|
|
23
|
+
/** From the first send to the outcome, or undefined when the call never ran or has not ended. */
|
|
24
|
+
export function toolCallDuration(row) {
|
|
25
|
+
return durationBetween(row.startedAt, row.settledAt);
|
|
26
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CapabilityManifest, SandboxToolName } from "@nylorun/core/define";
|
|
2
2
|
import type { SandboxManager, SandboxSessionRef } from "../sandbox/manager.js";
|
|
3
|
+
import { type DiskOp } from "../sandbox/disk-fs.js";
|
|
3
4
|
import type { SandboxSelectionReport } from "../sandbox/select.js";
|
|
4
5
|
import type { SandboxToolOutcome } from "../sandbox/tools.js";
|
|
5
6
|
import type { SessionStore } from "../store/types.js";
|
|
@@ -17,6 +18,8 @@ export interface WorkspacePort {
|
|
|
17
18
|
listFiles(session: SandboxSessionRef, capability: CapabilityManifest, dir: string, maxEntries: number, signal: AbortSignal): ReturnType<SandboxManager["listFiles"]>;
|
|
18
19
|
/** Reads a file of the workspace of `session` as bytes, at most `maxBytes` (`save_artifact`). */
|
|
19
20
|
readBytes(session: SandboxSessionRef, capability: CapabilityManifest, path: string, maxBytes: number, signal: AbortSignal): ReturnType<SandboxManager["readBytes"]>;
|
|
21
|
+
/** One operation on the disk of the workspace of `session` (R2d step 3). */
|
|
22
|
+
disk(session: SandboxSessionRef, capability: CapabilityManifest, op: DiskOp, signal: AbortSignal): ReturnType<SandboxManager["disk"]>;
|
|
20
23
|
report(): Promise<WorkspaceReport>;
|
|
21
24
|
/** One Tenant sweep pass: stop idle workspaces, remove those whose owner is gone. */
|
|
22
25
|
sweep(options: {
|
|
@@ -24,8 +27,8 @@ export interface WorkspacePort {
|
|
|
24
27
|
sessionExists: Exists;
|
|
25
28
|
sandboxExists: Exists;
|
|
26
29
|
}): Promise<void>;
|
|
27
|
-
/** Deletes sandbox resource `
|
|
28
|
-
removeSandbox(
|
|
30
|
+
/** Deletes sandbox resource `environmentId`'s workspace (the resource was deleted). */
|
|
31
|
+
removeSandbox(environmentId: string): Promise<void>;
|
|
29
32
|
/** Deletes every workspace with its files (a sandboxes reset). */
|
|
30
33
|
removeAll(): Promise<void>;
|
|
31
34
|
close(): Promise<void>;
|
|
@@ -48,6 +51,6 @@ export declare function remoteWorkspace(options: RemoteWorkspaceOptions): Worksp
|
|
|
48
51
|
* everything else goes to `base`. `podOf` says whether a sandbox resource is a pod sandbox.
|
|
49
52
|
*/
|
|
50
53
|
export declare function withPodWorkspaces(base: WorkspacePort, options: Omit<RemoteWorkspaceOptions, "pod"> & {
|
|
51
|
-
readonly isPod: (
|
|
54
|
+
readonly isPod: (environmentId: string) => Promise<boolean>;
|
|
52
55
|
}): WorkspacePort;
|
|
53
56
|
export {};
|