@rivet-dev/pi 0.5.0 → 0.5.1
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/dist/actions.d.ts +188 -77
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +276 -101
- package/dist/actions.js.map +1 -1
- package/dist/actor-config.d.ts +1 -2
- package/dist/actor-config.d.ts.map +1 -1
- package/dist/actor-config.js +1 -2
- package/dist/actor-config.js.map +1 -1
- package/dist/actor-sqlite.d.ts.map +1 -0
- package/dist/actor-sqlite.js.map +1 -0
- package/dist/actor.d.ts +21 -17
- package/dist/actor.d.ts.map +1 -1
- package/dist/actor.js +62 -34
- package/dist/actor.js.map +1 -1
- package/dist/credentials.d.ts +3 -4
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +11 -39
- package/dist/credentials.js.map +1 -1
- package/dist/drain.d.ts +1 -1
- package/dist/drain.js +1 -1
- package/dist/durable.d.ts +24 -0
- package/dist/durable.d.ts.map +1 -0
- package/dist/durable.js +5 -0
- package/dist/durable.js.map +1 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +4 -35
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +0 -70
- package/dist/models.js.map +1 -1
- package/dist/runtime.d.ts +84 -81
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +394 -406
- package/dist/runtime.js.map +1 -1
- package/dist/storage.d.ts +27 -45
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +28 -99
- package/dist/storage.js.map +1 -1
- package/dist/tracing.d.ts +4 -9
- package/dist/tracing.d.ts.map +1 -1
- package/dist/tracing.js +4 -40
- package/dist/tracing.js.map +1 -1
- package/dist/{durable/watches.d.ts → watches.d.ts} +1 -1
- package/dist/watches.d.ts.map +1 -0
- package/dist/{durable/watches.js → watches.js} +1 -1
- package/dist/watches.js.map +1 -0
- package/package.json +10 -18
- package/dist/durable/actions.d.ts +0 -192
- package/dist/durable/actions.d.ts.map +0 -1
- package/dist/durable/actions.js +0 -289
- package/dist/durable/actions.js.map +0 -1
- package/dist/durable/actor-sqlite.d.ts.map +0 -1
- package/dist/durable/actor-sqlite.js.map +0 -1
- package/dist/durable/actor.d.ts +0 -36
- package/dist/durable/actor.d.ts.map +0 -1
- package/dist/durable/actor.js +0 -107
- package/dist/durable/actor.js.map +0 -1
- package/dist/durable/env.d.ts +0 -13
- package/dist/durable/env.d.ts.map +0 -1
- package/dist/durable/env.js +0 -178
- package/dist/durable/env.js.map +0 -1
- package/dist/durable/errors.d.ts.map +0 -1
- package/dist/durable/errors.js.map +0 -1
- package/dist/durable/index.d.ts +0 -5
- package/dist/durable/index.d.ts.map +0 -1
- package/dist/durable/index.js +0 -2
- package/dist/durable/index.js.map +0 -1
- package/dist/durable/runtime.d.ts +0 -92
- package/dist/durable/runtime.d.ts.map +0 -1
- package/dist/durable/runtime.js +0 -435
- package/dist/durable/runtime.js.map +0 -1
- package/dist/durable/storage.d.ts +0 -31
- package/dist/durable/storage.d.ts.map +0 -1
- package/dist/durable/storage.js +0 -53
- package/dist/durable/storage.js.map +0 -1
- package/dist/durable/watches.d.ts.map +0 -1
- package/dist/durable/watches.js.map +0 -1
- package/dist/sandbox.d.ts +0 -27
- package/dist/sandbox.d.ts.map +0 -1
- package/dist/sandbox.js +0 -224
- package/dist/sandbox.js.map +0 -1
- /package/dist/{durable/actor-sqlite.d.ts → actor-sqlite.d.ts} +0 -0
- /package/dist/{durable/actor-sqlite.js → actor-sqlite.js} +0 -0
- /package/dist/{durable/errors.d.ts → errors.d.ts} +0 -0
- /package/dist/{durable/errors.js → errors.js} +0 -0
package/dist/runtime.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
|
+
import { AgentDoc, configure, Harness, } from "@earendil-works/pi-durable";
|
|
3
|
+
import { SqliteStorage } from "@earendil-works/pi-durable/storage/sqlite";
|
|
4
|
+
import { databaseEnv, sandboxEnv } from "@rivet-dev/sandbox-adapter/pi";
|
|
3
5
|
import { UserError } from "rivetkit";
|
|
6
|
+
import { actorSqlite } from "./actor-sqlite.js";
|
|
4
7
|
import { SourceCredentialStore } from "./credentials.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
8
|
+
import { toClientError } from "./errors.js";
|
|
9
|
+
import { createActorModelRuntime, emptyCredentialStore, } from "./models.js";
|
|
10
|
+
import { loadPiSandbox, savePiSandbox } from "./storage.js";
|
|
11
|
+
import { createConnectionWatches, reattachWatches, stopAllWatches, } from "./watches.js";
|
|
12
|
+
/**
|
|
13
|
+
* One minute, as in Cloudflare's Pi harness. A retry or poll wait longer than
|
|
14
|
+
* this lets the actor sleep, and a scheduled wake reopens Pi Durable at the
|
|
15
|
+
* deadline.
|
|
16
|
+
*/
|
|
17
|
+
const LONG_WAIT_MS = 60_000;
|
|
18
|
+
/** The internal action a scheduled wake calls. */
|
|
19
|
+
const WAKE_ACTION = "pi.wake";
|
|
18
20
|
export const PI_RUNTIME = Symbol.for("@rivet-dev/pi/runtime");
|
|
19
21
|
export function createPiRuntime() {
|
|
20
|
-
return {
|
|
22
|
+
return { closing: false, watches: createConnectionWatches() };
|
|
21
23
|
}
|
|
22
24
|
export function piRuntime(c) {
|
|
23
25
|
const runtime = c.vars?.[PI_RUNTIME];
|
|
@@ -26,466 +28,460 @@ export function piRuntime(c) {
|
|
|
26
28
|
}
|
|
27
29
|
return runtime;
|
|
28
30
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Opens the harness on wake without resuming interrupted work, so the app's
|
|
33
|
+
* `onWake` runs first. The caller calls `harness.resume()` after it. The wake
|
|
34
|
+
* context becomes the context of all background work.
|
|
35
|
+
*/
|
|
36
|
+
export async function openOnWake(c, options) {
|
|
37
|
+
piRuntime(c).actor = c;
|
|
38
|
+
return ensureHarness(c, options, { resume: false });
|
|
39
|
+
}
|
|
40
|
+
/** Returns the actor's Pi Durable harness, opening it from SQLite on first use. */
|
|
41
|
+
async function ensureHarness(c, options, open = { resume: true }) {
|
|
31
42
|
const runtime = piRuntime(c);
|
|
32
|
-
if (runtime.closing)
|
|
33
|
-
|
|
43
|
+
if (runtime.closing)
|
|
44
|
+
throw harnessClosed();
|
|
45
|
+
runtime.harness ??= openHarness(runtime.actor ?? c, runtime, options, open).catch((error) => {
|
|
46
|
+
runtime.harness = undefined;
|
|
47
|
+
throw toClientError(error);
|
|
48
|
+
});
|
|
49
|
+
return (await runtime.harness).harness;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Runs an action against the harness. A failed call that left Pi Durable
|
|
53
|
+
* unusable reopens it before the error reaches the client, so the next call
|
|
54
|
+
* works. Calls use `BACKGROUND_CONTEXT`, so a wait such as `prompt` lasts
|
|
55
|
+
* until the run settles or the action times out, including while a stopping
|
|
56
|
+
* actor drains the run. RivetKit's action signal also fires when the actor
|
|
57
|
+
* starts to stop, so it cannot cancel a wait.
|
|
58
|
+
*/
|
|
59
|
+
export async function withHarness(c, options, run) {
|
|
60
|
+
const harness = await ensureHarness(c, options);
|
|
61
|
+
try {
|
|
62
|
+
return await run(harness, BACKGROUND_CONTEXT);
|
|
34
63
|
}
|
|
35
|
-
|
|
36
|
-
runtime
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
64
|
+
catch (error) {
|
|
65
|
+
const runtime = piRuntime(c);
|
|
66
|
+
await recoverHarness(runtime.actor ?? c, runtime, options);
|
|
67
|
+
throw toClientError(error);
|
|
40
68
|
}
|
|
41
|
-
return runtime.ready;
|
|
42
69
|
}
|
|
43
70
|
/**
|
|
44
|
-
* Runs the app's own
|
|
45
|
-
*
|
|
46
|
-
* that never reads `c.pi` runs even
|
|
47
|
-
* `c.pi` throws the
|
|
48
|
-
*
|
|
49
|
-
*
|
|
71
|
+
* Runs one of the app's own actions or hooks with the harness on `c.pi`. A
|
|
72
|
+
* failure that left Pi Durable unusable reopens it. The error reaches the
|
|
73
|
+
* client as from a built-in action. Code that never reads `c.pi` runs even
|
|
74
|
+
* when Pi Durable is closed or cannot open; reading `c.pi` throws the cause.
|
|
75
|
+
* Shutdown hooks pass `opensHarness: false`, so they use a harness that is
|
|
76
|
+
* already open and never open and resume one only to close it.
|
|
50
77
|
*/
|
|
51
|
-
export async function
|
|
78
|
+
export async function withPiAccess(c, options, body, { opensHarness = true } = {}) {
|
|
52
79
|
const runtime = piRuntime(c);
|
|
53
|
-
// Hooks such as onDisconnect still run after
|
|
80
|
+
// Hooks such as onDisconnect still run after Pi Durable closed; only using c.pi fails.
|
|
54
81
|
if (runtime.closing)
|
|
55
|
-
return withUnavailablePi(c,
|
|
56
|
-
if (!
|
|
57
|
-
return withUnavailablePi(c,
|
|
82
|
+
return withUnavailablePi(c, harnessClosed(), body);
|
|
83
|
+
if (!opensHarness && !runtime.harness) {
|
|
84
|
+
return withUnavailablePi(c, harnessNotOpen(), body);
|
|
58
85
|
}
|
|
59
|
-
let
|
|
86
|
+
let harness;
|
|
60
87
|
try {
|
|
61
|
-
|
|
88
|
+
harness = await ensureHarness(c, options);
|
|
62
89
|
}
|
|
63
90
|
catch (error) {
|
|
64
91
|
c.log.warn({
|
|
65
|
-
msg: "pi
|
|
92
|
+
msg: "pi durable could not open; reading c.pi throws this error",
|
|
66
93
|
error,
|
|
67
94
|
});
|
|
68
95
|
return withUnavailablePi(c, error, body);
|
|
69
96
|
}
|
|
70
|
-
Object.defineProperty(c, "pi", { value:
|
|
71
|
-
let result;
|
|
97
|
+
Object.defineProperty(c, "pi", { value: harness, configurable: true });
|
|
72
98
|
try {
|
|
73
|
-
|
|
99
|
+
return await body();
|
|
74
100
|
}
|
|
75
101
|
catch (error) {
|
|
76
|
-
await
|
|
77
|
-
|
|
78
|
-
msg: "pi state write failed after an app action error",
|
|
79
|
-
error: writeError,
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
throw error;
|
|
102
|
+
await recoverHarness(runtime.actor ?? c, runtime, options);
|
|
103
|
+
throw toClientError(error);
|
|
83
104
|
}
|
|
84
|
-
await persistPiState(c, handle);
|
|
85
|
-
return result;
|
|
86
105
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Object.defineProperty(c, "pi", {
|
|
90
|
-
get: () => {
|
|
91
|
-
throw error;
|
|
92
|
-
},
|
|
93
|
-
configurable: true,
|
|
94
|
-
});
|
|
95
|
-
return body();
|
|
106
|
+
function harnessNotOpen() {
|
|
107
|
+
return new UserError("Pi Durable is not open in this actor generation, so this shutdown hook cannot use c.pi.");
|
|
96
108
|
}
|
|
97
|
-
function
|
|
98
|
-
return new UserError("
|
|
109
|
+
function harnessClosed() {
|
|
110
|
+
return new UserError("Pi Durable is closed because the actor is stopping.");
|
|
99
111
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
112
|
+
async function openHarness(c, runtime, options, open = { resume: true }) {
|
|
113
|
+
const storage = await SqliteStorage.open(actorSqlite(c.db));
|
|
114
|
+
const now = options.now ?? Date.now;
|
|
115
|
+
const harness = await Harness.open(storage, {
|
|
116
|
+
models: options.models ?? (await createModels(c, options)),
|
|
117
|
+
registry: options.registry,
|
|
118
|
+
settings: options.settings,
|
|
119
|
+
env: options.env ??
|
|
120
|
+
(options.sandbox
|
|
121
|
+
? sandboxEnvBuilder(c, runtime, options.sandbox)
|
|
122
|
+
: (target) => databaseEnv(c.db, {
|
|
123
|
+
id: `pi-files:${c.actorId}`,
|
|
124
|
+
cwd: target.cwd,
|
|
125
|
+
})),
|
|
126
|
+
conversationCreated: startingAgent(options),
|
|
127
|
+
now: options.now,
|
|
128
|
+
onReport: (error) => {
|
|
129
|
+
// A commit that failed after storage admitted it leaves Pi Durable
|
|
130
|
+
// unusable. Background work reports it here, with no action to catch it.
|
|
131
|
+
queueMicrotask(() => void recoverHarness(c, runtime, options));
|
|
132
|
+
options.onReport?.(error);
|
|
133
|
+
},
|
|
134
|
+
}, BACKGROUND_CONTEXT);
|
|
135
|
+
reportBlockedTasks(c, await harness.inspect(BACKGROUND_CONTEXT));
|
|
136
|
+
if (open.resume)
|
|
137
|
+
harness.resume();
|
|
138
|
+
const isBusy = async () => busyState(await harness.inspect(BACKGROUND_CONTEXT), now(), LONG_WAIT_MS)
|
|
139
|
+
.kind === "busy";
|
|
140
|
+
const stopBusyWatch = watchBusy(c, harness, now, LONG_WAIT_MS);
|
|
141
|
+
await reattachWatches(c, runtime.watches, harness, documentsByKind(options));
|
|
142
|
+
return { harness, stopBusyWatch, isBusy };
|
|
102
143
|
}
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"ls",
|
|
113
|
-
];
|
|
114
|
-
async function openPiSession(c, runtime, options) {
|
|
115
|
-
const { settings, sandbox: sandboxProvider, providers, apiKeys, model, scopedModels, credentials: credentialSource, ...sessionOptions } = options;
|
|
116
|
-
const loaded = await loadPiSession(c.db);
|
|
117
|
-
const stored = loaded && (await migratePiSession(c.db, loaded));
|
|
118
|
-
const sandbox = sandboxProvider ? lazySandbox(c, sandboxProvider) : undefined;
|
|
119
|
-
// A new session takes its working directory from the sandbox, so only its first open connects.
|
|
120
|
-
const firstSandbox = sandbox && !stored ? (await sandbox.connect()).sandbox : undefined;
|
|
121
|
-
const cwd = firstSandbox?.cwd ?? stored?.cwd ?? sessionOptions.cwd ?? process.cwd();
|
|
122
|
-
if (!isAbsolute(cwd)) {
|
|
123
|
-
throw new Error(`pi() cwd must be an absolute path, received ${cwd}`);
|
|
144
|
+
/** App documents by `kind`, for clients that name a document. */
|
|
145
|
+
export function documentsByKind(options) {
|
|
146
|
+
return new Map((options.documents ?? []).map((doc) => [doc.definition.kind, doc]));
|
|
147
|
+
}
|
|
148
|
+
/** Pi Durable refuses every call once a commit failed after storage admitted it. */
|
|
149
|
+
function isUsable(harness) {
|
|
150
|
+
try {
|
|
151
|
+
harness.subscribeClose(() => { })();
|
|
152
|
+
return true;
|
|
124
153
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const sessionManager = stored
|
|
128
|
-
? SessionManager.inMemory(cwd, undefined, toFileEntries(stored))
|
|
129
|
-
: SessionManager.inMemory(cwd);
|
|
130
|
-
const credentials = credentialSource
|
|
131
|
-
? new SourceCredentialStore(credentialSource(c))
|
|
132
|
-
: undefined;
|
|
133
|
-
const modelRuntime = await createActorModelRuntime({ providers, apiKeys }, credentials ?? emptyCredentialStore);
|
|
134
|
-
const resourceLoader = sessionOptions.resourceLoader ??
|
|
135
|
-
(await isolatedResourceLoader(cwd, sessionOptions.agentDir, settingsManager));
|
|
136
|
-
const { session, modelFallbackMessage } = await createAgentSession({
|
|
137
|
-
...sessionOptions,
|
|
138
|
-
cwd,
|
|
139
|
-
modelRuntime,
|
|
140
|
-
model: openingModel({ model, scopedModels }, modelRuntime, sessionManager.buildSessionContext().model),
|
|
141
|
-
settingsManager,
|
|
142
|
-
sessionManager,
|
|
143
|
-
resourceLoader,
|
|
144
|
-
customTools: connect
|
|
145
|
-
? [
|
|
146
|
-
...(sessionOptions.customTools ?? []),
|
|
147
|
-
...createSandboxTools(cwd, connect),
|
|
148
|
-
]
|
|
149
|
-
: sessionOptions.customTools,
|
|
150
|
-
excludeTools: [
|
|
151
|
-
...new Set([
|
|
152
|
-
...(sessionOptions.excludeTools ?? []),
|
|
153
|
-
...(sandbox ? ["powershell"] : PI_BUILT_IN_TOOLS),
|
|
154
|
-
]),
|
|
155
|
-
],
|
|
156
|
-
});
|
|
157
|
-
if (modelFallbackMessage) {
|
|
158
|
-
c.log.warn({ msg: "pi model fallback", detail: modelFallbackMessage });
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
159
156
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
persistedEntryCount: stored?.entries.length ?? 0,
|
|
170
|
-
credentials,
|
|
171
|
-
};
|
|
172
|
-
if (!stored) {
|
|
173
|
-
const header = sessionManager.getHeader();
|
|
174
|
-
if (!header) {
|
|
175
|
-
throw new Error("Pi did not create a session header");
|
|
176
|
-
}
|
|
177
|
-
await createPiSession(c.db, {
|
|
178
|
-
header,
|
|
179
|
-
cwd,
|
|
180
|
-
settings: JSON.parse(handle.persistedSettings),
|
|
181
|
-
});
|
|
157
|
+
}
|
|
158
|
+
/** Closes and reopens a harness that Pi Durable refuses to use. The reopened harness resumes from storage. */
|
|
159
|
+
async function recoverHarness(c, runtime, options) {
|
|
160
|
+
const current = runtime.harness;
|
|
161
|
+
if (!current || runtime.closing)
|
|
162
|
+
return;
|
|
163
|
+
let open;
|
|
164
|
+
try {
|
|
165
|
+
open = await current;
|
|
182
166
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
error,
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
// The run state tells the next wake whether a run was cut off.
|
|
195
|
-
if (event.type === "agent_start" ||
|
|
196
|
-
(event.type === "queue_update" && session.isStreaming)) {
|
|
197
|
-
void writeInOrder(c, runtime, () => savePiRunning(c.db, queuedMessages(session)));
|
|
198
|
-
}
|
|
199
|
-
else if (event.type === "agent_settled") {
|
|
200
|
-
void writeInOrder(c, runtime, () => savePiIdle(c.db));
|
|
201
|
-
}
|
|
167
|
+
catch {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (runtime.harness !== current || runtime.closing || isUsable(open.harness))
|
|
171
|
+
return;
|
|
172
|
+
c.log.warn({
|
|
173
|
+
msg: "pi durable stopped after a failed commit, reopening it from storage",
|
|
202
174
|
});
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
175
|
+
const reopened = (async () => {
|
|
176
|
+
await closeOpenHarness(open, runtime.watches);
|
|
177
|
+
return openHarness(c, runtime, options);
|
|
178
|
+
})().catch((error) => {
|
|
179
|
+
if (runtime.harness === reopened)
|
|
180
|
+
runtime.harness = undefined;
|
|
181
|
+
c.log.error({
|
|
182
|
+
msg: "pi durable could not reopen after a failed commit",
|
|
183
|
+
error,
|
|
184
|
+
});
|
|
185
|
+
throw toClientError(error);
|
|
209
186
|
});
|
|
210
|
-
|
|
187
|
+
runtime.harness = reopened;
|
|
188
|
+
await reopened.catch(() => { });
|
|
211
189
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
* other connect failure is thrown, so a temporary outage never replaces a
|
|
217
|
-
* sandbox. `pi()` and `piDurable()` both store the id in `pi_sandbox`.
|
|
218
|
-
*/
|
|
219
|
-
export async function connectSandbox(c, provider) {
|
|
220
|
-
const existing = await loadPiSandbox(c.db);
|
|
221
|
-
if (existing && existing.provider !== provider.name) {
|
|
222
|
-
throw new Error(`pi sandbox was created by provider ${existing.provider}, but the actor now uses ${provider.name}`);
|
|
223
|
-
}
|
|
224
|
-
if (existing) {
|
|
225
|
-
const sandbox = await provider.connect(c, existing.id);
|
|
226
|
-
if (sandbox)
|
|
227
|
-
return { provider, id: existing.id, sandbox };
|
|
228
|
-
c.log.warn({
|
|
229
|
-
msg: "pi sandbox no longer exists, creating a new one; files from the previous sandbox are lost",
|
|
230
|
-
provider: provider.name,
|
|
231
|
-
sandboxId: existing.id,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
const id = await provider.create(c);
|
|
235
|
-
await savePiSandbox(c.db, { provider: provider.name, id });
|
|
236
|
-
const sandbox = await provider.connect(c, id);
|
|
237
|
-
if (!sandbox) {
|
|
238
|
-
throw new Error(`pi sandbox ${provider.name}/${id} was not found right after it was created`);
|
|
239
|
-
}
|
|
240
|
-
return { provider, id, sandbox };
|
|
190
|
+
async function closeOpenHarness(open, watches) {
|
|
191
|
+
open.stopBusyWatch();
|
|
192
|
+
await stopAllWatches(watches);
|
|
193
|
+
await open.harness.close(BACKGROUND_CONTEXT);
|
|
241
194
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
async function isolatedResourceLoader(cwd, agentDir, settingsManager) {
|
|
248
|
-
const loader = new DefaultResourceLoader({
|
|
249
|
-
cwd,
|
|
250
|
-
agentDir: agentDir ?? getAgentDir(),
|
|
251
|
-
settingsManager,
|
|
252
|
-
noExtensions: true,
|
|
253
|
-
noSkills: true,
|
|
254
|
-
noPromptTemplates: true,
|
|
255
|
-
noContextFiles: true,
|
|
256
|
-
noThemes: true,
|
|
257
|
-
});
|
|
258
|
-
await loader.reload();
|
|
259
|
-
return loader;
|
|
195
|
+
async function createModels(c, options) {
|
|
196
|
+
const credentials = options.credentials
|
|
197
|
+
? new SourceCredentialStore(options.credentials(c))
|
|
198
|
+
: emptyCredentialStore;
|
|
199
|
+
return createActorModelRuntime({ providers: options.providers, apiKeys: options.apiKeys }, credentials);
|
|
260
200
|
}
|
|
261
|
-
/**
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
201
|
+
/** Parses `provider/modelId`. */
|
|
202
|
+
export function parseModelRef(name) {
|
|
203
|
+
const slash = name.indexOf("/");
|
|
204
|
+
if (slash <= 0 || slash === name.length - 1) {
|
|
205
|
+
throw new Error(`pi() model must be provider/modelId, received ${name}`);
|
|
206
|
+
}
|
|
207
|
+
return { provider: name.slice(0, slash), modelId: name.slice(slash + 1) };
|
|
266
208
|
}
|
|
267
209
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
210
|
+
* `model` and `thinkingLevel` start every new conversation: root, created, and
|
|
211
|
+
* forked. A fork already has its parent's agent, so it keeps it. The app's own
|
|
212
|
+
* `conversationCreated` runs after this.
|
|
271
213
|
*/
|
|
272
|
-
function
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const next = runtime.writes.then(write);
|
|
286
|
-
runtime.writes = next.catch((error) => {
|
|
287
|
-
c.log.error({ msg: "pi run state write failed", error });
|
|
288
|
-
});
|
|
289
|
-
return next;
|
|
290
|
-
}
|
|
291
|
-
function queuedMessages(session) {
|
|
292
|
-
return {
|
|
293
|
-
steering: [...session.getSteeringMessages()],
|
|
294
|
-
followUp: [...session.getFollowUpMessages()],
|
|
214
|
+
function startingAgent(options) {
|
|
215
|
+
const model = options.model === undefined ? undefined : parseModelRef(options.model);
|
|
216
|
+
const thinkingLevel = options.thinkingLevel;
|
|
217
|
+
const created = options.conversationCreated;
|
|
218
|
+
if (model === undefined && thinkingLevel === undefined)
|
|
219
|
+
return created;
|
|
220
|
+
return async (tx, conversation) => {
|
|
221
|
+
const agent = await tx.doc(AgentDoc, conversation.id);
|
|
222
|
+
await configure(tx, conversation.id, {
|
|
223
|
+
model: agent.model === undefined ? model : undefined,
|
|
224
|
+
thinkingLevel: agent.thinkingLevel === undefined ? thinkingLevel : undefined,
|
|
225
|
+
});
|
|
226
|
+
await created?.(tx, conversation);
|
|
295
227
|
};
|
|
296
228
|
}
|
|
297
229
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
230
|
+
* Builds the sandbox env without connecting, so a model request or a custom
|
|
231
|
+
* tool call never creates a sandbox. The sandbox connects on the first file
|
|
232
|
+
* or shell operation. A provider that does not report its working directory
|
|
233
|
+
* connects first, because the env needs that directory.
|
|
302
234
|
*/
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
235
|
+
function sandboxEnvBuilder(c, runtime, provider) {
|
|
236
|
+
const connect = async () => {
|
|
237
|
+
runtime.sandbox ??= connectSandbox(c, provider).catch((error) => {
|
|
238
|
+
runtime.sandbox = undefined;
|
|
239
|
+
throw error;
|
|
240
|
+
});
|
|
241
|
+
return (await runtime.sandbox).sandbox;
|
|
242
|
+
};
|
|
243
|
+
return async (target) => {
|
|
244
|
+
const root = provider.cwd ?? (await connect()).cwd;
|
|
245
|
+
return sandboxEnv(`${provider.name}:${c.actorId}`, root, connect, target.cwd);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function reportBlockedTasks(c, inspection) {
|
|
249
|
+
for (const task of inspection.tasks) {
|
|
250
|
+
if (task.state.kind !== "blocked")
|
|
251
|
+
continue;
|
|
252
|
+
c.log.warn({
|
|
253
|
+
msg: "pi durable task is blocked and does not keep the actor awake; inspect it with harness.inspect",
|
|
254
|
+
taskId: task.record.id,
|
|
255
|
+
kind: task.record.kind,
|
|
256
|
+
reason: task.state.reason,
|
|
257
|
+
error: task.state.error,
|
|
313
258
|
});
|
|
314
|
-
await writeInOrder(c, piRuntime(c), () => savePiIdle(c.db));
|
|
315
|
-
return;
|
|
316
259
|
}
|
|
317
|
-
c.log.info({
|
|
318
|
-
msg: "resuming the pi run that was active when the actor stopped",
|
|
319
|
-
lastRole,
|
|
320
|
-
});
|
|
321
|
-
const run = continueRun(handle.session, queue)
|
|
322
|
-
.then(() => persistPiState(c, handle))
|
|
323
|
-
.catch((error) => {
|
|
324
|
-
c.log.error({ msg: "resumed pi run failed", error });
|
|
325
|
-
});
|
|
326
|
-
void c.keepAwake(run);
|
|
327
260
|
}
|
|
328
261
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
262
|
+
* Whether live work should keep the actor awake. Tasks that run or are ready
|
|
263
|
+
* to run do. A task in a retry or poll wait longer than `longWaitMs` does not;
|
|
264
|
+
* the actor sleeps and a scheduled wake reopens it at the deadline. Waiting
|
|
265
|
+
* and completing tasks depend on other tasks, which decide. Blocked tasks
|
|
266
|
+
* never run, so they never keep the actor awake.
|
|
333
267
|
*/
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
for (const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
finally {
|
|
352
|
-
Reflect.deleteProperty(agent, "prompt");
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
function broadcastSessionEvent(c, event) {
|
|
356
|
-
try {
|
|
357
|
-
c.broadcast("event", event);
|
|
358
|
-
}
|
|
359
|
-
catch (error) {
|
|
360
|
-
if (isActorStoppingError(error))
|
|
361
|
-
return;
|
|
362
|
-
c.log.error({ msg: "failed to broadcast pi session event", error });
|
|
268
|
+
function busyState(inspection, now, longWaitMs) {
|
|
269
|
+
let until;
|
|
270
|
+
for (const task of inspection.tasks) {
|
|
271
|
+
switch (task.state.kind) {
|
|
272
|
+
case "running":
|
|
273
|
+
case "ready": {
|
|
274
|
+
const deadline = waitDeadline(task.record.state.checkpoint);
|
|
275
|
+
if (deadline === undefined || deadline - now <= longWaitMs)
|
|
276
|
+
return { kind: "busy" };
|
|
277
|
+
until = Math.min(until ?? deadline, deadline);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "waiting":
|
|
281
|
+
case "completing":
|
|
282
|
+
case "blocked":
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
363
285
|
}
|
|
286
|
+
return until === undefined ? { kind: "idle" } : { kind: "waiting", until };
|
|
364
287
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
288
|
+
/** The deadline of a task checkpoint in Pi's retry or poll phase. */
|
|
289
|
+
function waitDeadline(checkpoint) {
|
|
290
|
+
if (typeof checkpoint !== "object" ||
|
|
291
|
+
checkpoint === null ||
|
|
292
|
+
Array.isArray(checkpoint))
|
|
293
|
+
return undefined;
|
|
294
|
+
if (checkpoint.phase === "retry" && typeof checkpoint.until === "number")
|
|
295
|
+
return checkpoint.until;
|
|
296
|
+
if (checkpoint.phase === "poll" && typeof checkpoint.pollAt === "number")
|
|
297
|
+
return checkpoint.pollAt;
|
|
298
|
+
return undefined;
|
|
370
299
|
}
|
|
371
300
|
/**
|
|
372
|
-
*
|
|
373
|
-
*
|
|
301
|
+
* Keeps the actor awake while Pi Durable has live work, rechecking after
|
|
302
|
+
* every commit. Returns a function that stops watching.
|
|
374
303
|
*/
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Aborts the run in memory when RivetKit declares this actor generation lost.
|
|
386
|
-
* The engine may already run the next generation, which resumes the run, so
|
|
387
|
-
* the lost one must stop calling the model and running tools. A lost
|
|
388
|
-
* generation skips `onSleep`, and its storage rejects writes, so nothing is
|
|
389
|
-
* stored here. A normal stop fires the same signal and drains in `onSleep`.
|
|
390
|
-
* Takes the wake context, whose signal is the actor's, not one action's.
|
|
391
|
-
*/
|
|
392
|
-
export function stopRunWhenLost(c) {
|
|
393
|
-
const runtime = piRuntime(c);
|
|
394
|
-
c.abortSignal.addEventListener("abort", () => {
|
|
395
|
-
if (!isLost(c))
|
|
304
|
+
function watchBusy(c, harness, now, longWaitMs) {
|
|
305
|
+
let release;
|
|
306
|
+
let scheduledWakeAt;
|
|
307
|
+
let checking = false;
|
|
308
|
+
let dirty = false;
|
|
309
|
+
let stopped = false;
|
|
310
|
+
const stayAwake = () => {
|
|
311
|
+
if (release)
|
|
396
312
|
return;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
313
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
314
|
+
release = resolve;
|
|
315
|
+
void c.keepAwake(promise);
|
|
316
|
+
};
|
|
317
|
+
const letGo = () => {
|
|
318
|
+
release?.();
|
|
319
|
+
release = undefined;
|
|
320
|
+
};
|
|
321
|
+
const check = async () => {
|
|
322
|
+
if (checking) {
|
|
323
|
+
dirty = true;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
checking = true;
|
|
327
|
+
try {
|
|
328
|
+
do {
|
|
329
|
+
dirty = false;
|
|
330
|
+
const busy = busyState(await harness.inspect(BACKGROUND_CONTEXT), now(), longWaitMs);
|
|
331
|
+
if (stopped)
|
|
332
|
+
return;
|
|
333
|
+
if (busy.kind === "waiting" && busy.until !== scheduledWakeAt) {
|
|
334
|
+
// The actor may sleep through a long wait only once its wake is written.
|
|
335
|
+
stayAwake();
|
|
336
|
+
try {
|
|
337
|
+
await c.schedule.at(busy.until, WAKE_ACTION);
|
|
338
|
+
scheduledWakeAt = busy.until;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
// Pi's own timer then ends the wait, and its commit checks again.
|
|
342
|
+
c.log.warn({
|
|
343
|
+
msg: "pi durable could not schedule its wake, so the actor stays awake through the wait",
|
|
344
|
+
error,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (stopped)
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const canSleep = busy.kind === "idle" ||
|
|
351
|
+
(busy.kind === "waiting" && busy.until === scheduledWakeAt);
|
|
352
|
+
if (canSleep)
|
|
353
|
+
letGo();
|
|
354
|
+
else
|
|
355
|
+
stayAwake();
|
|
356
|
+
} while (dirty && !stopped);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (!stopped)
|
|
360
|
+
c.log.warn({ msg: "pi durable busy check failed", error });
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
checking = false;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
// Commit listeners must not call Pi Durable, so the check runs after the listener returns.
|
|
367
|
+
const unsubscribe = harness.subscribeCommits(() => queueMicrotask(() => void check()));
|
|
368
|
+
const stop = () => {
|
|
369
|
+
stopped = true;
|
|
370
|
+
unsubscribe();
|
|
371
|
+
letGo();
|
|
372
|
+
};
|
|
373
|
+
// Keep-awake only blocks idle sleep. Once shutdown starts it would only delay it.
|
|
374
|
+
c.abortSignal.addEventListener("abort", stop, { once: true });
|
|
375
|
+
void check();
|
|
376
|
+
return () => {
|
|
377
|
+
c.abortSignal.removeEventListener("abort", stop);
|
|
378
|
+
stop();
|
|
379
|
+
};
|
|
415
380
|
}
|
|
416
381
|
/**
|
|
417
|
-
* Waits until
|
|
418
|
-
* never has
|
|
419
|
-
* deadline ends before RivetKit's, so closing still fits in the grace
|
|
420
|
-
* Pi
|
|
382
|
+
* Waits until no run is active, or until `drainUntil` passes. Idle sleep
|
|
383
|
+
* never has live work, so this only waits on a forced stop, such as an upgrade.
|
|
384
|
+
* The deadline ends before RivetKit's, so closing still fits in the grace
|
|
385
|
+
* period. Pi's records are already stored, and the next wake resumes the work.
|
|
421
386
|
*/
|
|
422
|
-
async function drain(c,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
387
|
+
async function drain(c, open, drainUntil) {
|
|
388
|
+
let changed = () => { };
|
|
389
|
+
let timedOut = false;
|
|
390
|
+
const unsubscribe = open.harness.subscribeCommits(() => changed());
|
|
391
|
+
const timer = setTimeout(() => {
|
|
392
|
+
timedOut = true;
|
|
393
|
+
changed();
|
|
394
|
+
}, Math.max(0, drainUntil - Date.now()));
|
|
429
395
|
try {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
396
|
+
while (!timedOut) {
|
|
397
|
+
// Armed before the check, so a commit during the check is not missed.
|
|
398
|
+
const next = new Promise((resolve) => {
|
|
399
|
+
changed = resolve;
|
|
434
400
|
});
|
|
401
|
+
if (!(await open.isBusy()))
|
|
402
|
+
return;
|
|
403
|
+
await next;
|
|
435
404
|
}
|
|
405
|
+
c.log.warn({
|
|
406
|
+
msg: "pi durable run still active at the end of the grace period; it resumes on wake",
|
|
407
|
+
});
|
|
436
408
|
}
|
|
437
409
|
finally {
|
|
438
410
|
clearTimeout(timer);
|
|
411
|
+
unsubscribe();
|
|
439
412
|
}
|
|
440
413
|
}
|
|
441
414
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* resume it. Then lets extensions shut down, flushes settings and entries,
|
|
447
|
-
* and suspends the sandbox on sleep, or destroys it on destroy.
|
|
415
|
+
* Closes Pi Durable for this actor generation. On sleep it drains first. On
|
|
416
|
+
* destroy it stops running work at once. Records of interrupted work stay
|
|
417
|
+
* running, and the next open resumes them. Then suspends the sandbox on
|
|
418
|
+
* sleep, or destroys it on destroy.
|
|
448
419
|
*/
|
|
449
|
-
export async function
|
|
420
|
+
export async function closeHarness(c, options, stop) {
|
|
450
421
|
const runtime = piRuntime(c);
|
|
451
|
-
const ready = runtime.ready;
|
|
452
422
|
const errors = [];
|
|
453
|
-
let
|
|
423
|
+
let open;
|
|
454
424
|
try {
|
|
455
|
-
|
|
425
|
+
open = await runtime.harness;
|
|
456
426
|
}
|
|
457
427
|
catch { }
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
await
|
|
461
|
-
? drain(c, open.session, stop.drainUntil)
|
|
462
|
-
: open.session.abort());
|
|
428
|
+
// Destroy has no next generation to resume in, so it stops running work at once.
|
|
429
|
+
if (open && stop.reason === "sleep") {
|
|
430
|
+
await drain(c, open, stop.drainUntil).catch((error) => errors.push(error));
|
|
463
431
|
}
|
|
464
432
|
runtime.closing = true;
|
|
465
|
-
runtime.
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
await attempt(errors, async () => {
|
|
469
|
-
if (open.session.hasExtensionHandlers("session_shutdown")) {
|
|
470
|
-
await open.session.extensionRunner.emit({
|
|
471
|
-
type: "session_shutdown",
|
|
472
|
-
reason: "quit",
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
await attempt(errors, () => persistPiState(c, open));
|
|
477
|
-
open.session.dispose();
|
|
478
|
-
c.log.info({ msg: "pi session closed", sessionId: open.session.sessionId });
|
|
433
|
+
runtime.harness = undefined;
|
|
434
|
+
if (open) {
|
|
435
|
+
await closeOpenHarness(open, runtime.watches).catch((error) => errors.push(error));
|
|
479
436
|
}
|
|
480
437
|
const provider = options.sandbox;
|
|
481
438
|
if (provider) {
|
|
482
|
-
await
|
|
439
|
+
await closeSandbox(c, provider, runtime.sandbox, stop).catch((error) => errors.push(error));
|
|
483
440
|
}
|
|
484
441
|
if (errors.length === 1)
|
|
485
442
|
throw errors[0];
|
|
486
|
-
if (errors.length > 1)
|
|
487
|
-
throw new AggregateError(errors, "pi
|
|
443
|
+
if (errors.length > 1)
|
|
444
|
+
throw new AggregateError(errors, "pi durable shutdown failed");
|
|
445
|
+
}
|
|
446
|
+
/** Runs `body` with a `c.pi` that throws `error` when read. */
|
|
447
|
+
export function withUnavailablePi(c, error, body) {
|
|
448
|
+
Object.defineProperty(c, "pi", {
|
|
449
|
+
get: () => {
|
|
450
|
+
throw error;
|
|
451
|
+
},
|
|
452
|
+
configurable: true,
|
|
453
|
+
});
|
|
454
|
+
return body();
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Connects to the actor's sandbox, creating one when none is stored or the
|
|
458
|
+
* provider reports the stored one no longer exists. A new sandbox id is saved
|
|
459
|
+
* as soon as `create` returns, so a failure later in the start reuses it. Any
|
|
460
|
+
* other connect failure is thrown, so a temporary outage never replaces a
|
|
461
|
+
* sandbox.
|
|
462
|
+
*/
|
|
463
|
+
export async function connectSandbox(c, provider) {
|
|
464
|
+
const existing = await loadPiSandbox(c.db);
|
|
465
|
+
if (existing && existing.provider !== provider.name) {
|
|
466
|
+
throw new Error(`pi sandbox was created by provider ${existing.provider}, but the actor now uses ${provider.name}`);
|
|
488
467
|
}
|
|
468
|
+
if (existing) {
|
|
469
|
+
const sandbox = await provider.connect(c, existing.id);
|
|
470
|
+
if (sandbox)
|
|
471
|
+
return { provider, id: existing.id, sandbox };
|
|
472
|
+
c.log.warn({
|
|
473
|
+
msg: "pi sandbox no longer exists, creating a new one; files from the previous sandbox are lost",
|
|
474
|
+
provider: provider.name,
|
|
475
|
+
sandboxId: existing.id,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
const id = await provider.create(c);
|
|
479
|
+
await savePiSandbox(c.db, { provider: provider.name, id });
|
|
480
|
+
const sandbox = await provider.connect(c, id);
|
|
481
|
+
if (!sandbox) {
|
|
482
|
+
throw new Error(`pi sandbox ${provider.name}/${id} was not found right after it was created`);
|
|
483
|
+
}
|
|
484
|
+
return { provider, id, sandbox };
|
|
489
485
|
}
|
|
490
486
|
/**
|
|
491
487
|
* Suspends the sandbox this generation connected when the actor sleeps. On
|
|
@@ -507,12 +503,4 @@ export async function closeSandbox(c, provider, connection, stop) {
|
|
|
507
503
|
if (id !== undefined)
|
|
508
504
|
await provider.destroy(c, id);
|
|
509
505
|
}
|
|
510
|
-
async function attempt(errors, operation) {
|
|
511
|
-
try {
|
|
512
|
-
await operation();
|
|
513
|
-
}
|
|
514
|
-
catch (error) {
|
|
515
|
-
errors.push(error);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
506
|
//# sourceMappingURL=runtime.js.map
|