@nylorun/runtime 0.15.1-beta → 0.16.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +33 -6
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/replication/pgoutput.js +3 -1
- 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 +37 -3
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.js +5 -1
- package/dist/api/components.d.ts +2673 -655
- package/dist/api/components.js +17 -1
- 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 +4 -0
- package/dist/api/http/define.d.ts +10 -0
- package/dist/api/http/define.js +2 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +518 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +152 -0
- package/dist/api/http/routes/sessions.js +3 -1
- package/dist/api/http/routes/tenant.js +36 -2
- 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/model-gate.d.ts +6 -2
- 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/app.js +23 -2
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +46 -1
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +135 -27
- package/dist/host/stack-config.d.ts +101 -1
- package/dist/host/stack-config.js +199 -8
- 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 +15245 -8850
- 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/meta/_journal.json +28 -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 +894 -0
- package/dist/store/postgres/schema.js +130 -2
- package/dist/store/postgres/store.js +289 -4
- package/dist/store/types.d.ts +143 -6
- package/dist/streams/control.d.ts +2 -0
- package/dist/streams/control.js +5 -0
- package/dist/streams/types.d.ts +10 -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/commands.js +30 -5
- package/dist/tenant/context.d.ts +64 -5
- package/dist/tenant/effects.d.ts +19 -13
- package/dist/tenant/effects.js +5 -382
- 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/module.js +1 -0
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- 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 +38 -0
- package/dist/tenant/runtime.js +120 -32
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +5 -0
- package/dist/tenant/sessions.js +28 -1
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +7 -0
- package/dist/tenant/streams.js +24 -1
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/tokens.js +9 -2
- package/dist/tenant/types.d.ts +18 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A harness's connection to core (F6.2): a WebSocket to the Harness API listener, with the
|
|
3
|
+
* harness credential (`Authorization: Bearer`, `NYLORUN_HARNESS_TOKEN`) and the API version
|
|
4
|
+
* header. Each connection runs one `createHarness`: it says `hello`, leases runs, and answers
|
|
5
|
+
* core's `workspace.*` requests (`serve`). When the connection is lost its runs stop (core gives
|
|
6
|
+
* their segments to the next advance, which resumes them by replay) and the client connects
|
|
7
|
+
* again after a jittered backoff of 0.5 to 10 s.
|
|
8
|
+
*/
|
|
9
|
+
import { createChannel } from "@nylorun/core/harness-api";
|
|
10
|
+
import { createHarness } from "@nylorun/harness/api";
|
|
11
|
+
import { WebSocket } from "ws";
|
|
12
|
+
import { HARNESS_API_HEADER, HARNESS_MAX_PAYLOAD, wsPort } from "./ws-port.js";
|
|
13
|
+
export function connectHarness(options) {
|
|
14
|
+
const { logger } = options;
|
|
15
|
+
const backoff = options.backoff ?? { minMs: 500, maxMs: 10_000 };
|
|
16
|
+
let stopped = false;
|
|
17
|
+
let connected = false;
|
|
18
|
+
let current;
|
|
19
|
+
let wake;
|
|
20
|
+
let markReady;
|
|
21
|
+
const ready = new Promise((resolve) => (markReady = resolve));
|
|
22
|
+
const open = async () => {
|
|
23
|
+
const token = typeof options.token === "string" ? options.token : await options.token();
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const socket = new WebSocket(options.url, {
|
|
26
|
+
headers: { authorization: `Bearer ${token}`, [HARNESS_API_HEADER]: "1" },
|
|
27
|
+
maxPayload: HARNESS_MAX_PAYLOAD,
|
|
28
|
+
handshakeTimeout: 10_000,
|
|
29
|
+
});
|
|
30
|
+
socket.once("open", () => {
|
|
31
|
+
socket.removeAllListeners("error");
|
|
32
|
+
resolve(socket);
|
|
33
|
+
});
|
|
34
|
+
socket.once("unexpected-response", (_request, response) => {
|
|
35
|
+
socket.removeAllListeners("error");
|
|
36
|
+
socket.on("error", () => undefined);
|
|
37
|
+
reject(new Error(`The Harness API refused the connection: ${response.statusCode ?? 0}`));
|
|
38
|
+
response.resume();
|
|
39
|
+
socket.terminate();
|
|
40
|
+
});
|
|
41
|
+
socket.once("error", reject);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const pause = (ms) => new Promise((resolve) => {
|
|
45
|
+
const timer = setTimeout(done, ms);
|
|
46
|
+
timer.unref();
|
|
47
|
+
function done() {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
wake = undefined;
|
|
50
|
+
resolve();
|
|
51
|
+
}
|
|
52
|
+
wake = done;
|
|
53
|
+
});
|
|
54
|
+
const session = async () => {
|
|
55
|
+
const socket = await open();
|
|
56
|
+
if (stopped)
|
|
57
|
+
return void socket.close(1000, "harness stopped");
|
|
58
|
+
const channel = createChannel(wsPort(socket, options.liveness ?? {}), { validate: true });
|
|
59
|
+
if (options.serve)
|
|
60
|
+
channel.handle(options.serve);
|
|
61
|
+
const harness = createHarness({ ...options, channel, logger });
|
|
62
|
+
current = { channel, harness };
|
|
63
|
+
const lost = new Promise((resolve) => channel.onClose(resolve));
|
|
64
|
+
try {
|
|
65
|
+
await harness.start();
|
|
66
|
+
connected = true;
|
|
67
|
+
markReady();
|
|
68
|
+
logger.info("harness_connected", { url: options.url });
|
|
69
|
+
const reason = await lost;
|
|
70
|
+
logger.warn("harness_connection_lost", { reason });
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
connected = false;
|
|
74
|
+
// The connection is gone: its runs were aborted with it; let them wind down.
|
|
75
|
+
if (!stopped)
|
|
76
|
+
await harness.stop(5_000);
|
|
77
|
+
channel.close("reconnecting");
|
|
78
|
+
current = undefined;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const loop = (async () => {
|
|
82
|
+
let attempt = 0;
|
|
83
|
+
while (!stopped) {
|
|
84
|
+
const started = Date.now();
|
|
85
|
+
try {
|
|
86
|
+
await session();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (!stopped)
|
|
90
|
+
logger.warn("harness_connect_failed", {
|
|
91
|
+
url: options.url,
|
|
92
|
+
message: error instanceof Error ? error.message : String(error),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (stopped)
|
|
96
|
+
break;
|
|
97
|
+
// A connection that lasted resets the backoff.
|
|
98
|
+
if (Date.now() - started > backoff.maxMs)
|
|
99
|
+
attempt = 0;
|
|
100
|
+
const ceiling = Math.min(backoff.maxMs, backoff.minMs * 2 ** attempt);
|
|
101
|
+
attempt += 1;
|
|
102
|
+
await pause(backoff.minMs + Math.random() * Math.max(0, ceiling - backoff.minMs));
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
return {
|
|
106
|
+
ready,
|
|
107
|
+
get connected() {
|
|
108
|
+
return connected;
|
|
109
|
+
},
|
|
110
|
+
channel: () => current?.channel,
|
|
111
|
+
async stop(waitMs = 10_000) {
|
|
112
|
+
stopped = true;
|
|
113
|
+
wake?.();
|
|
114
|
+
const live = current;
|
|
115
|
+
if (live) {
|
|
116
|
+
await live.harness.stop(waitMs);
|
|
117
|
+
live.channel.close("harness stopped");
|
|
118
|
+
}
|
|
119
|
+
await loop;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Harness API over a WebSocket (F6.2): one JSON text message per frame. Either end watches
|
|
3
|
+
* the connection: core pings every `pingMs` (30 s), the harness answers (`ws` does it), and an
|
|
4
|
+
* end that hears nothing from the other for `silenceMs` (90 s) drops the connection. A message
|
|
5
|
+
* that is not JSON closes it (1007); `createChannel({ validate: true })` checks the rest.
|
|
6
|
+
*/
|
|
7
|
+
import type { Port } from "@nylorun/core/harness-api";
|
|
8
|
+
import { WebSocket } from "ws";
|
|
9
|
+
/** The URL path of the Harness API, and the header naming its version. */
|
|
10
|
+
export declare const HARNESS_API_PATH = "/nylorun/harness/v1";
|
|
11
|
+
export declare const HARNESS_API_HEADER = "nylorun-harness-api";
|
|
12
|
+
/** Largest frame either end accepts. */
|
|
13
|
+
export declare const HARNESS_MAX_PAYLOAD: number;
|
|
14
|
+
export declare const DEFAULT_PING_MS = 30000;
|
|
15
|
+
export declare const DEFAULT_SILENCE_MS = 90000;
|
|
16
|
+
export interface WsPortOptions {
|
|
17
|
+
/** Send pings (core's end). */
|
|
18
|
+
readonly ping?: boolean;
|
|
19
|
+
readonly pingMs?: number;
|
|
20
|
+
readonly silenceMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function wsPort(socket: WebSocket, options?: WsPortOptions): Port;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
/** The URL path of the Harness API, and the header naming its version. */
|
|
3
|
+
export const HARNESS_API_PATH = "/nylorun/harness/v1";
|
|
4
|
+
export const HARNESS_API_HEADER = "nylorun-harness-api";
|
|
5
|
+
/** Largest frame either end accepts. */
|
|
6
|
+
export const HARNESS_MAX_PAYLOAD = 64 * 1024 * 1024;
|
|
7
|
+
export const DEFAULT_PING_MS = 30_000;
|
|
8
|
+
export const DEFAULT_SILENCE_MS = 90_000;
|
|
9
|
+
export function wsPort(socket, options = {}) {
|
|
10
|
+
const frames = [];
|
|
11
|
+
const closes = [];
|
|
12
|
+
const silenceMs = options.silenceMs ?? DEFAULT_SILENCE_MS;
|
|
13
|
+
let lastHeard = Date.now();
|
|
14
|
+
let closed = false;
|
|
15
|
+
let reason = "closed";
|
|
16
|
+
const heard = () => {
|
|
17
|
+
lastHeard = Date.now();
|
|
18
|
+
};
|
|
19
|
+
const watch = setInterval(() => {
|
|
20
|
+
if (Date.now() - lastHeard > silenceMs) {
|
|
21
|
+
reason = `nothing heard for ${Math.round(silenceMs / 1000)} s`;
|
|
22
|
+
socket.terminate();
|
|
23
|
+
}
|
|
24
|
+
else if (options.ping && socket.readyState === WebSocket.OPEN)
|
|
25
|
+
socket.ping();
|
|
26
|
+
}, Math.min(options.pingMs ?? DEFAULT_PING_MS, silenceMs));
|
|
27
|
+
watch.unref();
|
|
28
|
+
socket.on("ping", heard);
|
|
29
|
+
socket.on("pong", heard);
|
|
30
|
+
socket.on("message", (data, binary) => {
|
|
31
|
+
heard();
|
|
32
|
+
let frame;
|
|
33
|
+
try {
|
|
34
|
+
if (binary)
|
|
35
|
+
throw new Error("binary message");
|
|
36
|
+
frame = JSON.parse(data.toString());
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
reason = "a message that is not a JSON frame";
|
|
40
|
+
socket.close(1007, "Harness API frames are JSON text");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const listener of frames)
|
|
44
|
+
listener(frame);
|
|
45
|
+
});
|
|
46
|
+
socket.on("error", (error) => {
|
|
47
|
+
reason = error.message;
|
|
48
|
+
});
|
|
49
|
+
socket.on("close", (code, why) => {
|
|
50
|
+
clearInterval(watch);
|
|
51
|
+
if (closed)
|
|
52
|
+
return;
|
|
53
|
+
closed = true;
|
|
54
|
+
const said = why.toString();
|
|
55
|
+
const message = said || (code === 1000 ? reason : `${reason} (${code})`);
|
|
56
|
+
for (const listener of closes)
|
|
57
|
+
listener(message);
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
send(frame) {
|
|
61
|
+
if (socket.readyState === WebSocket.OPEN)
|
|
62
|
+
socket.send(JSON.stringify(frame));
|
|
63
|
+
},
|
|
64
|
+
onFrame(listener) {
|
|
65
|
+
frames.push(listener);
|
|
66
|
+
},
|
|
67
|
+
onClose(listener) {
|
|
68
|
+
closes.push(listener);
|
|
69
|
+
},
|
|
70
|
+
close(why = "closed") {
|
|
71
|
+
reason = why;
|
|
72
|
+
if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING)
|
|
73
|
+
return;
|
|
74
|
+
// A close reason is at most 123 bytes.
|
|
75
|
+
socket.close(1000, Buffer.from(why).subarray(0, 120).toString());
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type MemoryPortsOptions } from "@nylorun/core/harness-api";
|
|
2
|
+
import type { ModelRoute } from "../harness/calls.js";
|
|
3
|
+
import type { McpPool } from "../mcp/pool.js";
|
|
4
|
+
import type { SandboxManager } from "../sandbox/manager.js";
|
|
5
|
+
import type { SandboxBackend } from "../sandbox/types.js";
|
|
6
|
+
import type { TenantContext } from "../tenant/context.js";
|
|
7
|
+
/** Where the Tenant's model calls go. */
|
|
8
|
+
export declare function modelRouteOf(ctx: TenantContext): ModelRoute;
|
|
9
|
+
export interface InProcessHarness {
|
|
10
|
+
/** Aborts its runs with `shutdown`, waits for them at most `waitMs`, and detaches. */
|
|
11
|
+
stop(waitMs?: number): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function startInProcessHarness(ctx: TenantContext, tools: {
|
|
14
|
+
mcp: McpPool;
|
|
15
|
+
sandbox: SandboxManager;
|
|
16
|
+
}, options?: MemoryPortsOptions): Promise<InProcessHarness>;
|
|
17
|
+
/**
|
|
18
|
+
* A harness service over a loopback WebSocket (tests): what `--service harness` runs, in this
|
|
19
|
+
* process. Its workspaces live under the Tenant's `sandboxes` directory. Remote MCP servers go
|
|
20
|
+
* through the Tenant's Tool Gate when it opens them, else they are authorized in this process.
|
|
21
|
+
*/
|
|
22
|
+
export declare function startLoopbackHarness(ctx: TenantContext, options?: {
|
|
23
|
+
sandboxBackends?: readonly SandboxBackend[];
|
|
24
|
+
}): Promise<InProcessHarness>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tenant's own harness (D37): `@nylorun/harness/api` in this process, attached to the
|
|
3
|
+
* Tenant's Harness API server over a memory channel, with the Tenant's model gate, MCP pool
|
|
4
|
+
* and SandboxManager as its executors. `json` sends every frame through JSON and validates it,
|
|
5
|
+
* as a socket will (tests).
|
|
6
|
+
*
|
|
7
|
+
* `startLoopbackHarness` (tests, `harness: "ws"`) runs a harness service in this process instead:
|
|
8
|
+
* over a WebSocket to a Harness API listener on 127.0.0.1, with its own MCP pool and
|
|
9
|
+
* SandboxManager and no store, as `--service harness` runs one.
|
|
10
|
+
*/
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { memoryChannels } from "@nylorun/core/harness-api";
|
|
13
|
+
import { createHarness } from "@nylorun/harness/api";
|
|
14
|
+
import { harnessExecutors } from "../harness/executors.js";
|
|
15
|
+
import { startHarnessService } from "../harness/service.js";
|
|
16
|
+
import { authorize } from "../tenant/effects.js";
|
|
17
|
+
import { startHarnessListener } from "./ws-server.js";
|
|
18
|
+
/** Where the Tenant's model calls go. */
|
|
19
|
+
export function modelRouteOf(ctx) {
|
|
20
|
+
return {
|
|
21
|
+
tenantId: ctx.config.tenantId,
|
|
22
|
+
modelGate: ctx.modelGate,
|
|
23
|
+
modelProvider: ctx.modelProvider,
|
|
24
|
+
useVaultModel: ctx.useVaultModel,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function startInProcessHarness(ctx, tools, options = {}) {
|
|
28
|
+
const channels = memoryChannels(options);
|
|
29
|
+
const detach = ctx.harness.attach(channels.core, { name: "in-process" });
|
|
30
|
+
const harness = createHarness({
|
|
31
|
+
channel: channels.harness,
|
|
32
|
+
executors: harnessExecutors({
|
|
33
|
+
model: modelRouteOf(ctx),
|
|
34
|
+
toolGate: ctx.toolGate,
|
|
35
|
+
mcp: tools.mcp,
|
|
36
|
+
sandbox: () => tools.sandbox,
|
|
37
|
+
}),
|
|
38
|
+
logger: ctx.config.logger,
|
|
39
|
+
name: "in-process",
|
|
40
|
+
});
|
|
41
|
+
await harness.start();
|
|
42
|
+
return {
|
|
43
|
+
async stop(waitMs) {
|
|
44
|
+
await harness.stop(waitMs);
|
|
45
|
+
channels.harness.close("stopped");
|
|
46
|
+
detach();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A harness service over a loopback WebSocket (tests): what `--service harness` runs, in this
|
|
52
|
+
* process. Its workspaces live under the Tenant's `sandboxes` directory. Remote MCP servers go
|
|
53
|
+
* through the Tenant's Tool Gate when it opens them, else they are authorized in this process.
|
|
54
|
+
*/
|
|
55
|
+
export async function startLoopbackHarness(ctx, options = {}) {
|
|
56
|
+
const token = randomBytes(32).toString("hex");
|
|
57
|
+
const listener = await startHarnessListener({
|
|
58
|
+
host: "127.0.0.1",
|
|
59
|
+
port: 0,
|
|
60
|
+
allowedHosts: [],
|
|
61
|
+
token,
|
|
62
|
+
attach: async () => (ctx.closed ? undefined : (channel, peer) => ctx.harness.attach(channel, peer)),
|
|
63
|
+
logger: ctx.config.logger,
|
|
64
|
+
});
|
|
65
|
+
const route = modelRouteOf(ctx);
|
|
66
|
+
const service = startHarnessService({
|
|
67
|
+
url: listener.url,
|
|
68
|
+
token,
|
|
69
|
+
paths: { sandboxes: ctx.config.paths.sandboxes, pluginData: ctx.config.paths.pluginData },
|
|
70
|
+
childEnv: ctx.config.childEnv,
|
|
71
|
+
modelGate: route.modelGate,
|
|
72
|
+
modelProvider: route.modelProvider,
|
|
73
|
+
useVaultModel: route.useVaultModel,
|
|
74
|
+
toolGate: ctx.toolGate,
|
|
75
|
+
...(ctx.toolGate.openMcp
|
|
76
|
+
? {}
|
|
77
|
+
: { authorize: (sessionId, request) => authorize(ctx, sessionId, request) }),
|
|
78
|
+
...(options.sandboxBackends ? { sandboxBackends: options.sandboxBackends } : {}),
|
|
79
|
+
logger: quietLogger(ctx),
|
|
80
|
+
name: "loopback",
|
|
81
|
+
ephemeral: ctx.config.mode === "ephemeral",
|
|
82
|
+
backoff: { minMs: 50, maxMs: 500 },
|
|
83
|
+
});
|
|
84
|
+
let timer;
|
|
85
|
+
try {
|
|
86
|
+
await Promise.race([
|
|
87
|
+
service.client.ready,
|
|
88
|
+
new Promise((_, reject) => {
|
|
89
|
+
timer = setTimeout(() => reject(new Error("The loopback harness did not connect")), 10_000);
|
|
90
|
+
}),
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
await service.stop(0);
|
|
95
|
+
await listener.close();
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
async stop(waitMs) {
|
|
103
|
+
await service.stop(waitMs);
|
|
104
|
+
await listener.close();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** The Tenant's logger, which never throws: a harness may log after the Tenant's log is gone. */
|
|
109
|
+
function quietLogger(ctx) {
|
|
110
|
+
const quiet = (level) => (message, fields) => {
|
|
111
|
+
try {
|
|
112
|
+
ctx.config.logger[level](message, fields);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* nowhere to log */
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return { info: quiet("info"), warn: quiet("warn") };
|
|
119
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Record seam of the Harness API: core's journal for the effects of a run. `recordIntent`
|
|
3
|
+
* journals an effect before anything runs it, and either answers it from the journal, hands it
|
|
4
|
+
* to core's own executors (Actions, flow work, delegation), or tells the harness to `execute`
|
|
5
|
+
* it. `recordOutcome` records what the harness's call returned. Both run under the advance's
|
|
6
|
+
* lease: every write is epoch-checked (`ownedSession`), and a lost epoch writes nothing.
|
|
7
|
+
*
|
|
8
|
+
* Rows store the request's hash (`requestHash`) and, for a model call, the request without its
|
|
9
|
+
* prompt: drift is a hash compare. Rows written before carry their full request, hashed on read.
|
|
10
|
+
*/
|
|
11
|
+
import { type EffectIntent, type IntentAnswer, type OutcomeAnswer, type RecordedOutcome } from "@nylorun/core/harness-api";
|
|
12
|
+
import { type Lease, type Session, type TenantContext } from "../tenant/context.js";
|
|
13
|
+
import type { Tx } from "../store/types.js";
|
|
14
|
+
/** What a run's journal writes are scoped by: the advance's lease and its signal. */
|
|
15
|
+
export interface RecordScope {
|
|
16
|
+
readonly ctx: TenantContext;
|
|
17
|
+
readonly lease: Lease;
|
|
18
|
+
readonly signal: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
/** The hash a journal row's request was recorded with. */
|
|
21
|
+
export declare function hashOf(row: {
|
|
22
|
+
request: unknown;
|
|
23
|
+
requestHash?: string;
|
|
24
|
+
}): string;
|
|
25
|
+
/** The completed outcomes of a segment, which a run replays without asking. */
|
|
26
|
+
export declare function bulkOutcomes(t: Tx, session: Pick<Session, "id">, checkpoint: {
|
|
27
|
+
turnId: string;
|
|
28
|
+
segment: number;
|
|
29
|
+
}): Promise<RecordedOutcome[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Journals one effect. `effect` is the engine's request; a model intent may come without its
|
|
32
|
+
* prompt, and then `requestHash` is the only record of it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function recordIntent(scope: RecordScope, effect: EffectIntent, requestHash: string): Promise<IntentAnswer>;
|
|
35
|
+
/**
|
|
36
|
+
* Records the outcome of a call the harness ran: `value`, with its transcript events, or the
|
|
37
|
+
* `error` that left it uncertain. Only a cancel discards an outcome in hand (§10.7): after any
|
|
38
|
+
* other abort (shutdown, deadline) it is recorded, so the next advance replays it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function recordOutcome(scope: RecordScope, effectId: string, result: {
|
|
41
|
+
value: unknown;
|
|
42
|
+
} | {
|
|
43
|
+
error: string;
|
|
44
|
+
}): Promise<OutcomeAnswer>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Record seam of the Harness API: core's journal for the effects of a run. `recordIntent`
|
|
3
|
+
* journals an effect before anything runs it, and either answers it from the journal, hands it
|
|
4
|
+
* to core's own executors (Actions, flow work, delegation), or tells the harness to `execute`
|
|
5
|
+
* it. `recordOutcome` records what the harness's call returned. Both run under the advance's
|
|
6
|
+
* lease: every write is epoch-checked (`ownedSession`), and a lost epoch writes nothing.
|
|
7
|
+
*
|
|
8
|
+
* Rows store the request's hash (`requestHash`) and, for a model call, the request without its
|
|
9
|
+
* prompt: drift is a hash compare. Rows written before carry their full request, hashed on read.
|
|
10
|
+
*/
|
|
11
|
+
import { HarnessApiError, effectRequestHash, } from "@nylorun/core/harness-api";
|
|
12
|
+
import { isFlowEffect, isFlowToolEffect } from "../core/flow-host.js";
|
|
13
|
+
import { sandboxCapabilityOf } from "../sandbox/capability.js";
|
|
14
|
+
import { isSaveArtifactCall } from "../harness/calls.js";
|
|
15
|
+
import { callSaveArtifact } from "../tenant/artifact-tool.js";
|
|
16
|
+
import { isOwnershipLost } from "../store/ownership.js";
|
|
17
|
+
import { manifestFor, mcpToolOf } from "../mcp/snapshot.js";
|
|
18
|
+
import { ownedSession } from "../tenant/context.js";
|
|
19
|
+
import { actionTarget, pinnedTool } from "../tenant/session.js";
|
|
20
|
+
import { offerAction } from "../tenant/delivery.js";
|
|
21
|
+
import { isRemoteMcpEffect, linkedOutcome, recoversMcpCalls, recoversModelCalls, resolveNewFlowEffect, } from "../tenant/effects.js";
|
|
22
|
+
import { assistantMessage, contextCompacted, modelFailed, toolCompleted, toolIds, } from "../tenant/transcript.js";
|
|
23
|
+
import { abortKind } from "../tenant/worker.js";
|
|
24
|
+
const TURN_CANCELLED = () => new HarnessApiError("turn_cancelled", "Turn cancelled");
|
|
25
|
+
/** The hash a journal row's request was recorded with. */
|
|
26
|
+
export function hashOf(row) {
|
|
27
|
+
return row.requestHash ?? effectRequestHash(row.request);
|
|
28
|
+
}
|
|
29
|
+
/** The completed outcomes of a segment, which a run replays without asking. */
|
|
30
|
+
export async function bulkOutcomes(t, session, checkpoint) {
|
|
31
|
+
const prefix = `${checkpoint.turnId}:${checkpoint.segment}:`;
|
|
32
|
+
const rows = await t.effectsForTurn(session.id, checkpoint.turnId, ["completed"]);
|
|
33
|
+
return rows
|
|
34
|
+
.filter((row) => row.request.effectId.startsWith(prefix) && row.outcome)
|
|
35
|
+
.map((row) => ({ effectId: row.request.effectId, requestHash: hashOf(row), outcome: row.outcome }));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Journals one effect. `effect` is the engine's request; a model intent may come without its
|
|
39
|
+
* prompt, and then `requestHash` is the only record of it.
|
|
40
|
+
*/
|
|
41
|
+
export async function recordIntent(scope, effect, requestHash) {
|
|
42
|
+
const { ctx, lease, signal } = scope;
|
|
43
|
+
const request = effect;
|
|
44
|
+
const answer = await ctx.store.tx(async (t) => {
|
|
45
|
+
const s = await ownedSession(t, lease, request.sessionId);
|
|
46
|
+
if (s.status === "cancelled" || s.activeTurnId !== request.turnId)
|
|
47
|
+
throw TURN_CANCELLED();
|
|
48
|
+
// An aborted advance starts no effect; the advance decides what the abort means.
|
|
49
|
+
signal.throwIfAborted();
|
|
50
|
+
const existing = await t.get("effects", request.effectId);
|
|
51
|
+
if (existing) {
|
|
52
|
+
if (hashOf(existing) !== requestHash)
|
|
53
|
+
throw new HarnessApiError("effect_drift", "Effect identity request drift");
|
|
54
|
+
if (existing.status === "completed")
|
|
55
|
+
return { status: "completed", outcome: existing.outcome };
|
|
56
|
+
// A call its previous owner left running at the gate: re-send it, same key and request,
|
|
57
|
+
// to join it or collect its outcome (P1.2 for model calls, F4.1 for remote MCP calls).
|
|
58
|
+
if (existing.status === "invoking" && request.kind === "model" && recoversModelCalls(ctx))
|
|
59
|
+
return { status: "execute", rejoin: true };
|
|
60
|
+
if (existing.status === "invoking" && recoversMcpCalls(ctx) && isRemoteMcpEffect(s, existing.request))
|
|
61
|
+
return { status: "execute", rejoin: true };
|
|
62
|
+
if (request.kind === "agent" && existing.status === "pending" && existing.agentSessionId) {
|
|
63
|
+
const agent = await t.get("sessions", existing.agentSessionId);
|
|
64
|
+
const outcome = await linkedOutcome(t, existing, agent);
|
|
65
|
+
if (outcome) {
|
|
66
|
+
existing.status = "completed";
|
|
67
|
+
existing.outcome = outcome;
|
|
68
|
+
await t.put("effects", request.effectId, existing);
|
|
69
|
+
return { status: "completed", outcome };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
status: existing.status === "uncertain" || existing.status === "invoking" ? "uncertain" : "pending",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (request.kind === "agent" ||
|
|
77
|
+
request.kind === "fn" ||
|
|
78
|
+
request.kind === "verify" ||
|
|
79
|
+
isFlowToolEffect(request))
|
|
80
|
+
return "flow";
|
|
81
|
+
if (request.kind !== "model" && effectRequestHash(request) !== requestHash)
|
|
82
|
+
throw new HarnessApiError("invalid", "The request hash does not match the request");
|
|
83
|
+
if (request.kind === "delegation") {
|
|
84
|
+
// Lifecycle points of an agent used as a tool: journaled once, so replays never re-emit.
|
|
85
|
+
const outcome = { value: null };
|
|
86
|
+
await t.put("effects", request.effectId, { request, requestHash, status: "completed", outcome });
|
|
87
|
+
const settled = request.effectId.endsWith(":settled");
|
|
88
|
+
const callId = request.context?.callId;
|
|
89
|
+
await t.event(s.id, s.activeTurnId, settled ? "delegation.completed" : "delegation.started", {
|
|
90
|
+
agent: request.agent,
|
|
91
|
+
...(typeof callId === "string" ? { callId } : {}),
|
|
92
|
+
...request.input,
|
|
93
|
+
});
|
|
94
|
+
return { status: "completed", outcome };
|
|
95
|
+
}
|
|
96
|
+
const agentManifest = manifestFor(s.manifest, request.agent);
|
|
97
|
+
if (!agentManifest)
|
|
98
|
+
throw new Error(`Agent '${request.agent?.id ?? ""}' is not used as a tool`);
|
|
99
|
+
const executed = request.kind === "model" ||
|
|
100
|
+
(request.kind === "tool" &&
|
|
101
|
+
(mcpToolOf(s.mcpSnapshot, request) !== undefined ||
|
|
102
|
+
sandboxCapabilityOf(agentManifest, request.capabilityId, request.toolName) !== undefined ||
|
|
103
|
+
// `save_artifact` (F8.1) runs beside the sandbox tools.
|
|
104
|
+
isSaveArtifactCall(agentManifest, request)));
|
|
105
|
+
await t.put("effects", request.effectId, {
|
|
106
|
+
request: storedRequest(request),
|
|
107
|
+
requestHash,
|
|
108
|
+
status: executed ? "invoking" : "pending",
|
|
109
|
+
});
|
|
110
|
+
// `save_artifact` writes the Tenant's artifacts: core runs it, reading the file through the
|
|
111
|
+
// workspace capability, wherever the sandbox is.
|
|
112
|
+
if (executed && request.kind === "tool" && isSaveArtifactCall(agentManifest, request))
|
|
113
|
+
return "save";
|
|
114
|
+
if (executed)
|
|
115
|
+
return { status: "execute" };
|
|
116
|
+
await offerActionFor(scope, t, s, agentManifest, request);
|
|
117
|
+
return { status: "pending" };
|
|
118
|
+
});
|
|
119
|
+
if (answer === "save")
|
|
120
|
+
return saveArtifact(scope, request);
|
|
121
|
+
if (answer !== "flow")
|
|
122
|
+
return answer;
|
|
123
|
+
if (!isFlowEffect(request))
|
|
124
|
+
return { status: "pending" };
|
|
125
|
+
return resolveNewFlowEffect(ctx, request, signal, lease);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Runs `save_artifact` (F8.1) for the run and records its outcome, as a harness would record a
|
|
129
|
+
* call it ran: the run gets the outcome as the intent's answer.
|
|
130
|
+
*/
|
|
131
|
+
async function saveArtifact(scope, request) {
|
|
132
|
+
let value;
|
|
133
|
+
try {
|
|
134
|
+
value = await callSaveArtifact(scope.ctx, request, scope.signal);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (isOwnershipLost(error))
|
|
138
|
+
throw error;
|
|
139
|
+
return recordOutcome(scope, request.effectId, {
|
|
140
|
+
error: error instanceof Error ? error.message : String(error),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return recordOutcome(scope, request.effectId, { value });
|
|
144
|
+
}
|
|
145
|
+
/** A tool or hook Action for the agent's endpoint, in the intent's transaction. */
|
|
146
|
+
async function offerActionFor(scope, t, s, agentManifest, request) {
|
|
147
|
+
const tool = request.kind === "tool" ? pinnedTool(agentManifest, request.capabilityId, request.toolName) : undefined;
|
|
148
|
+
const base = {
|
|
149
|
+
actionId: request.effectId,
|
|
150
|
+
sessionId: request.sessionId,
|
|
151
|
+
turnId: request.turnId,
|
|
152
|
+
agentId: request.agentId,
|
|
153
|
+
manifestHash: request.manifestHash,
|
|
154
|
+
implementationVersion: s.implementationVersion,
|
|
155
|
+
input: request.input,
|
|
156
|
+
context: request.context,
|
|
157
|
+
status: "pending",
|
|
158
|
+
generation: 0,
|
|
159
|
+
...(request.agent ? { agent: request.agent } : {}),
|
|
160
|
+
};
|
|
161
|
+
const action = request.kind === "hook"
|
|
162
|
+
? {
|
|
163
|
+
...base,
|
|
164
|
+
kind: "hook",
|
|
165
|
+
hook: {
|
|
166
|
+
at: request.hook.at,
|
|
167
|
+
scope: request.hook.scope,
|
|
168
|
+
capabilityIds: [...request.hook.capabilityIds],
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
: {
|
|
172
|
+
...base,
|
|
173
|
+
kind: "tool",
|
|
174
|
+
capabilityId: request.capabilityId,
|
|
175
|
+
toolName: request.toolName,
|
|
176
|
+
...(tool?.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
177
|
+
...(tool?.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
178
|
+
};
|
|
179
|
+
await t.put("actions", action.actionId, action);
|
|
180
|
+
await t.event(s.id, s.activeTurnId, "action.pending", {
|
|
181
|
+
actionId: action.actionId,
|
|
182
|
+
kind: action.kind,
|
|
183
|
+
...actionTarget(action),
|
|
184
|
+
...(action.kind === "tool" ? toolIds(request.context) : {}),
|
|
185
|
+
input: action.input,
|
|
186
|
+
});
|
|
187
|
+
await offerAction(t, scope.ctx, action);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Records the outcome of a call the harness ran: `value`, with its transcript events, or the
|
|
191
|
+
* `error` that left it uncertain. Only a cancel discards an outcome in hand (§10.7): after any
|
|
192
|
+
* other abort (shutdown, deadline) it is recorded, so the next advance replays it.
|
|
193
|
+
*/
|
|
194
|
+
export async function recordOutcome(scope, effectId, result) {
|
|
195
|
+
const { ctx, lease, signal } = scope;
|
|
196
|
+
if ("error" in result) {
|
|
197
|
+
await ctx.store.tx(async (t) => {
|
|
198
|
+
const s = await t.assertEpoch(lease.sessionId, lease.epoch);
|
|
199
|
+
const effect = await t.get("effects", effectId);
|
|
200
|
+
if (!effect)
|
|
201
|
+
return;
|
|
202
|
+
const { request } = effect;
|
|
203
|
+
effect.status = "uncertain";
|
|
204
|
+
effect.error = result.error;
|
|
205
|
+
await t.put("effects", effectId, effect);
|
|
206
|
+
if (s && s.status !== "cancelled" && s.activeTurnId === request.turnId)
|
|
207
|
+
await t.event(s.id, request.turnId, "effect.uncertain", { effectId, message: result.error });
|
|
208
|
+
});
|
|
209
|
+
return { status: "uncertain" };
|
|
210
|
+
}
|
|
211
|
+
const { value } = result;
|
|
212
|
+
return ctx.store.tx(async (t) => {
|
|
213
|
+
const s = await t.assertEpoch(lease.sessionId, lease.epoch);
|
|
214
|
+
const effect = await t.get("effects", effectId);
|
|
215
|
+
if (!effect)
|
|
216
|
+
throw new HarnessApiError("invalid", `Effect ${effectId} was never journaled`);
|
|
217
|
+
const { request } = effect;
|
|
218
|
+
if (s.status === "cancelled" || s.activeTurnId !== request.turnId || abortKind(signal) === "cancel")
|
|
219
|
+
throw TURN_CANCELLED();
|
|
220
|
+
effect.status = "completed";
|
|
221
|
+
effect.outcome = { value };
|
|
222
|
+
await t.put("effects", effectId, effect);
|
|
223
|
+
const model = request.kind === "model";
|
|
224
|
+
const failed = model ? modelFailed(request, value) : undefined;
|
|
225
|
+
// A summary call is never an assistant message; the last one publishes context.compacted.
|
|
226
|
+
const summarizing = model && Boolean(request.context.compaction);
|
|
227
|
+
const compacted = summarizing && !failed ? contextCompacted(request, value) : undefined;
|
|
228
|
+
const transcript = failed || summarizing
|
|
229
|
+
? undefined
|
|
230
|
+
: model
|
|
231
|
+
? assistantMessage(request, value)
|
|
232
|
+
: toolCompleted(request, value);
|
|
233
|
+
if (failed)
|
|
234
|
+
await t.event(s.id, request.turnId, "model.failed", failed);
|
|
235
|
+
if (compacted)
|
|
236
|
+
await t.event(s.id, request.turnId, "context.compacted", compacted);
|
|
237
|
+
if (transcript && model)
|
|
238
|
+
await t.event(s.id, request.turnId, "message.assistant", transcript);
|
|
239
|
+
else if (transcript)
|
|
240
|
+
await t.event(s.id, request.turnId, "tool.completed", transcript);
|
|
241
|
+
return { status: "completed", outcome: effect.outcome };
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/** What a journal row keeps of a request: a model call without its prompt. */
|
|
245
|
+
function storedRequest(request) {
|
|
246
|
+
if (request.kind !== "model" || request.input === undefined)
|
|
247
|
+
return request;
|
|
248
|
+
const { input: _, ...stored } = request;
|
|
249
|
+
return stored;
|
|
250
|
+
}
|